Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2297cd2f5 | ||
|
|
cc3035d3ee | ||
|
|
e0b57922b0 | ||
|
|
54c1a6f1ed |
@@ -909,14 +909,6 @@
|
||||
"BenefitTierReferralDiscountFactorDescription": "The proportion of the referee's taker fees to be discounted",
|
||||
"BenefitTierReferralRewardFactor": "Referral reward factor",
|
||||
"BenefitTierReferralRewardFactorDescription": "The proportion of the referee's taker fees to be rewarded to the referrer",
|
||||
"BenefitTierMinimumActivityStreak": "Minimum activity streak",
|
||||
"BenefitTierMinimumActivityStreakDescription": "The minimum number of times the party needs to have completed the activity",
|
||||
"BenefitTierMinimumQuantumBalance": "Minimum quantum balance",
|
||||
"BenefitTierMinimumQuantumBalanceDescription": "The minimum amount of the vesting token to qualify",
|
||||
"BenefitTierVestingMultiplier": "Vesting multiplier",
|
||||
"BenefitTierVestingMultiplierDescription": "Vesting multiplier for the tier",
|
||||
"BenefitTierRewardMultiplier": "Reward multiplier",
|
||||
"BenefitTierRewardMultiplierDescription": "The multiplier",
|
||||
"StakingTiers": "Staking tiers",
|
||||
"StakingTierMinimumStakedTokens": "Minimum staked tokens",
|
||||
"StakingTierMinimumStakedTokensDescription": "Required number of governance tokens ($VEGA) a referrer must have staked to receive the multiplier",
|
||||
|
||||
+2
-2
@@ -36,13 +36,13 @@ describe('ProposalReferralProgramDetails helper functions', () => {
|
||||
it('should format referral discount factor correctly', () => {
|
||||
const input = '0.05';
|
||||
const formatted = formatReferralDiscountFactor(input);
|
||||
expect(formatted).toBe('5%');
|
||||
expect(formatted).toBe('5.00%');
|
||||
});
|
||||
|
||||
it('should format referral reward factor correctly', () => {
|
||||
const input = '0.1';
|
||||
const formatted = formatReferralRewardFactor(input);
|
||||
expect(formatted).toBe('10%');
|
||||
expect(formatted).toBe('10.00%');
|
||||
});
|
||||
|
||||
it('should format minimum staked tokens correctly', () => {
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
export * from './proposal-update-benefit-tiers-details';
|
||||
-158
@@ -1,158 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalUpdateBenefitTiers } from './proposal-update-benefit-tiers-details';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
|
||||
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||
useAppState: () => ({
|
||||
appState: {
|
||||
decimals: 2,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockVestingBenefitTierProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({
|
||||
tiers: [
|
||||
{
|
||||
minimum_quantum_balance: '10000',
|
||||
reward_multiplier: '0.05',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '500000000000',
|
||||
reward_multiplier: '0.1',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '10000000000000',
|
||||
reward_multiplier: '10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mockActivityStreakBenefitTierProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({
|
||||
tiers: [
|
||||
{
|
||||
minimum_activity_streak: '10000',
|
||||
vesting_multiplier: '5',
|
||||
reward_multiplier: '0.1',
|
||||
},
|
||||
{
|
||||
minimum_activity_streak: '10000000000000',
|
||||
vesting_multiplier: '100',
|
||||
reward_multiplier: '10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
describe('ProposalUpdateBenefitTiers', () => {
|
||||
it('should not render if proposal is null', () => {
|
||||
render(<ProposalUpdateBenefitTiers proposal={null} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if __typename is not UpdateNetworkParameter', () => {
|
||||
const updateMarketProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
},
|
||||
},
|
||||
});
|
||||
render(<ProposalUpdateBenefitTiers proposal={updateMarketProposal} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if there are no relevant fields', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if there are relevant fields that are empty', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render a valid vesting benefit tier proposal', () => {
|
||||
render(
|
||||
<ProposalUpdateBenefitTiers proposal={mockVestingBenefitTierProposal} />
|
||||
);
|
||||
|
||||
// 3 tiers in the sample data
|
||||
expect(screen.getByText('Tier 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tier 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tier 3')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getAllByText('Minimum quantum balance').length).toBe(3);
|
||||
expect(screen.getAllByText('Reward multiplier').length).toBe(3);
|
||||
|
||||
expect(screen.getByText('0.00000000000001')).toBeInTheDocument();
|
||||
expect(screen.getByText('0.05x')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('0.0000005')).toBeInTheDocument();
|
||||
expect(screen.getByText('0.1x')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('0.00001')).toBeInTheDocument();
|
||||
expect(screen.getByText('10x')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a valid activity streak benefit tier proposal', () => {
|
||||
render(
|
||||
<ProposalUpdateBenefitTiers
|
||||
proposal={mockActivityStreakBenefitTierProposal}
|
||||
/>
|
||||
);
|
||||
|
||||
// 3 tiers in the sample data
|
||||
expect(screen.getByText('Tier 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tier 2')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getAllByText('Minimum activity streak').length).toBe(2);
|
||||
expect(screen.getAllByText('Vesting multiplier').length).toBe(2);
|
||||
expect(screen.getAllByText('Reward multiplier').length).toBe(2);
|
||||
|
||||
expect(screen.getByText('10000')).toBeInTheDocument();
|
||||
expect(screen.getByText('5x')).toBeInTheDocument();
|
||||
expect(screen.getByText('0.1x')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('10000000000000')).toBeInTheDocument();
|
||||
expect(screen.getByText('100x')).toBeInTheDocument();
|
||||
expect(screen.getByText('10x')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
RoundedWrapper,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
formatMinimumStakedTokens,
|
||||
formatReferralRewardMultiplier,
|
||||
} from '../proposal-referral-program-details';
|
||||
import { formatNumberPercentage } from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
// These types are not generated as it's not known how dynamic these are
|
||||
type VestingBenefitTier = {
|
||||
minimum_quantum_balance: string;
|
||||
reward_multiplier: string;
|
||||
};
|
||||
|
||||
type ActivityStreakBenefitTier = {
|
||||
minimum_activity_streak: number;
|
||||
reward_multiplier: string;
|
||||
vesting_multiplier: string;
|
||||
};
|
||||
|
||||
export type BenefitTiers =
|
||||
| Array<ActivityStreakBenefitTier>
|
||||
| Array<VestingBenefitTier>;
|
||||
|
||||
export function getBenefitTiers(json: string): BenefitTiers {
|
||||
try {
|
||||
const parsed = JSON.parse(json);
|
||||
return parsed.tiers;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export const formatVolumeDiscountFactor = (value: string) => {
|
||||
return formatNumberPercentage(new BigNumber(value).times(100));
|
||||
};
|
||||
|
||||
interface ProposalReferralProgramDetailsProps {
|
||||
proposal: ProposalQuery['proposal'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Special rendered for network proposals that change any benefit tiers,
|
||||
* which is detected by:
|
||||
* 1) it being a network parameter change
|
||||
* 2) the name of the field ending in `.benefitTiers`
|
||||
*
|
||||
* It only renders known fields so that they can be formatted correctly.
|
||||
*/
|
||||
export const ProposalUpdateBenefitTiers = ({
|
||||
proposal,
|
||||
}: ProposalReferralProgramDetailsProps) => {
|
||||
const { t } = useTranslation();
|
||||
if (
|
||||
proposal?.terms?.change?.__typename !== 'UpdateNetworkParameter' ||
|
||||
proposal?.terms?.change?.networkParameter.key.slice(-13) !== '.benefitTiers'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const benefitTiersString = proposal?.terms?.change?.networkParameter.value;
|
||||
const benefitTiers = getBenefitTiers(benefitTiersString);
|
||||
|
||||
if (!benefitTiers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="proposal-update-benefit-tiers">
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
{benefitTiers && (
|
||||
<div
|
||||
className="mb-6"
|
||||
data-testid="proposal-volume-discount-program-benefit-tiers"
|
||||
>
|
||||
<h3 className="mb-3 uppercase font-semibold text-lg">
|
||||
{t('BenefitTiers')}
|
||||
</h3>
|
||||
<KeyValueTable>
|
||||
{benefitTiers
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Number(a.reward_multiplier) - Number(b.reward_multiplier)
|
||||
)
|
||||
.map((benefitTier, index) => (
|
||||
<div className="mb-4" key={index}>
|
||||
<h4 className="font-semibold uppercase">
|
||||
Tier {index + 1}
|
||||
</h4>
|
||||
{'minimum_activity_streak' in benefitTier && (
|
||||
<KeyValueTableRow
|
||||
data-testid={`mas-${benefitTier.reward_multiplier}`}
|
||||
>
|
||||
<Tooltip
|
||||
description={t(
|
||||
'BenefitTierMinimumActivityStreakDescription'
|
||||
)}
|
||||
>
|
||||
<span>{t('BenefitTierMinimumActivityStreak')}</span>
|
||||
</Tooltip>
|
||||
|
||||
{benefitTier.minimum_activity_streak}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
{'minimum_quantum_balance' in benefitTier && (
|
||||
<KeyValueTableRow
|
||||
data-testid={`mqb-${benefitTier.reward_multiplier}`}
|
||||
>
|
||||
<Tooltip
|
||||
description={t(
|
||||
'BenefitTierMinimumQuantumBalanceDescription'
|
||||
)}
|
||||
>
|
||||
<span>{t('BenefitTierMinimumQuantumBalance')}</span>
|
||||
</Tooltip>
|
||||
|
||||
{formatMinimumStakedTokens(
|
||||
benefitTier.minimum_quantum_balance,
|
||||
18
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
{'vesting_multiplier' in benefitTier && (
|
||||
<KeyValueTableRow
|
||||
data-testid={`vm-${benefitTier.reward_multiplier}`}
|
||||
>
|
||||
<Tooltip
|
||||
description={t('BenefitTierVestingMultiplier')}
|
||||
>
|
||||
<span>{t('BenefitTierVestingMultiplier')}</span>
|
||||
</Tooltip>
|
||||
{formatReferralRewardMultiplier(
|
||||
benefitTier.vesting_multiplier
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
{'reward_multiplier' in benefitTier && (
|
||||
<KeyValueTableRow
|
||||
data-testid={`rm-${benefitTier.reward_multiplier}`}
|
||||
>
|
||||
<Tooltip description={t('BenefitTierRewardMultiplier')}>
|
||||
<span>{t('BenefitTierRewardMultiplier')}</span>
|
||||
</Tooltip>
|
||||
{formatReferralRewardMultiplier(
|
||||
benefitTier.reward_multiplier
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</KeyValueTable>
|
||||
</div>
|
||||
)}
|
||||
</RoundedWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
ProposalTransferDetails,
|
||||
} from '../proposal-transfer';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
|
||||
|
||||
export interface ProposalProps {
|
||||
proposal: ProposalQuery['proposal'];
|
||||
@@ -244,14 +243,6 @@ export const Proposal = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'UpdateNetworkParameter' &&
|
||||
proposal.terms.change.networkParameter.key.slice(-13) ===
|
||||
'.benefitTiers' && (
|
||||
<div className="mb-4">
|
||||
<ProposalUpdateBenefitTiers proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{governanceTransferDetails}
|
||||
|
||||
<div className="mb-10">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"next",
|
||||
"next/core-web-vitals"
|
||||
],
|
||||
"ignorePatterns": ["!**/*", "__generated__", ".next"],
|
||||
"ignorePatterns": ["!**/*", "__generated__"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
|
||||
@@ -18,7 +18,6 @@ import { Routes } from '../../lib/links';
|
||||
import { useTransactionEventSubscription } from '@vegaprotocol/web3';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Statistics } from './referral-statistics';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
|
||||
const RELOAD_DELAY = 3000;
|
||||
|
||||
@@ -33,7 +32,6 @@ const validateCode = (value: string) => {
|
||||
};
|
||||
|
||||
export const ApplyCodeForm = () => {
|
||||
const program = useReferralProgram();
|
||||
const navigate = useNavigate();
|
||||
const openWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
@@ -239,7 +237,7 @@ export const ApplyCodeForm = () => {
|
||||
{previewData ? (
|
||||
<div className="mt-10">
|
||||
<h2 className="text-2xl mb-5">{t('You are joining')}</h2>
|
||||
<Statistics data={previewData} program={program} as="referee" />
|
||||
<Statistics data={previewData} as="referee" />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
query Referees($code: ID!, $aggregationEpochs: Int) {
|
||||
referralSetReferees(id: $code, aggregationEpochs: $aggregationEpochs) {
|
||||
query Referees($code: ID!, $aggregationDays: Int) {
|
||||
referralSetReferees(id: $code, aggregationDays: $aggregationDays) {
|
||||
edges {
|
||||
node {
|
||||
referralSetId
|
||||
|
||||
@@ -10,7 +10,6 @@ query ReferralSetStats($code: ID!, $epoch: Int) {
|
||||
referralSetRunningNotionalTakerVolume
|
||||
rewardsMultiplier
|
||||
rewardsFactorMultiplier
|
||||
referrerTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type RefereesQueryVariables = Types.Exact<{
|
||||
code: Types.Scalars['ID'];
|
||||
aggregationEpochs?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
aggregationDays?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ export type RefereesQuery = { __typename?: 'Query', referralSetReferees: { __typ
|
||||
|
||||
|
||||
export const RefereesDocument = gql`
|
||||
query Referees($code: ID!, $aggregationEpochs: Int) {
|
||||
referralSetReferees(id: $code, aggregationEpochs: $aggregationEpochs) {
|
||||
query Referees($code: ID!, $aggregationDays: Int) {
|
||||
referralSetReferees(id: $code, aggregationDays: $aggregationDays) {
|
||||
edges {
|
||||
node {
|
||||
referralSetId
|
||||
@@ -42,7 +42,7 @@ export const RefereesDocument = gql`
|
||||
* const { data, loading, error } = useRefereesQuery({
|
||||
* variables: {
|
||||
* code: // value for 'code'
|
||||
* aggregationEpochs: // value for 'aggregationEpochs'
|
||||
* aggregationDays: // value for 'aggregationDays'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
+1
-2
@@ -9,7 +9,7 @@ export type ReferralSetStatsQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string, referrerTakerVolume: string } } | null> } };
|
||||
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string } } | null> } };
|
||||
|
||||
|
||||
export const ReferralSetStatsDocument = gql`
|
||||
@@ -25,7 +25,6 @@ export const ReferralSetStatsDocument = gql`
|
||||
referralSetRunningNotionalTakerVolume
|
||||
rewardsMultiplier
|
||||
rewardsFactorMultiplier
|
||||
referrerTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import compact from 'lodash/compact';
|
||||
import type { ReferralSetsQueryVariables } from './__generated__/ReferralSets';
|
||||
import { useReferralSetsQuery } from './__generated__/ReferralSets';
|
||||
|
||||
export const DEFAULT_AGGREGATION_DAYS = 30;
|
||||
const DEFAULT_AGGREGATION_DAYS = 30;
|
||||
|
||||
export type Role = 'referrer' | 'referee';
|
||||
type UseReferralArgs = (
|
||||
| { code: string }
|
||||
| { pubKey: string | null; role: Role }
|
||||
) & {
|
||||
aggregationEpochs?: number;
|
||||
aggregationDays?: number;
|
||||
};
|
||||
|
||||
const prepareVariables = (
|
||||
@@ -70,9 +70,9 @@ export const useReferral = (args: UseReferralArgs) => {
|
||||
} = useRefereesQuery({
|
||||
variables: {
|
||||
code: referralSet?.id as string,
|
||||
aggregationEpochs:
|
||||
args.aggregationEpochs !== null
|
||||
? args.aggregationEpochs
|
||||
aggregationDays:
|
||||
args.aggregationDays != null
|
||||
? args.aggregationDays
|
||||
: DEFAULT_AGGREGATION_DAYS,
|
||||
},
|
||||
skip: !referralSet?.id,
|
||||
|
||||
@@ -3,12 +3,10 @@ import {
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
ExternalLink,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { DEFAULT_AGGREGATION_DAYS, useReferral } from './hooks/use-referral';
|
||||
import { useReferral } from './hooks/use-referral';
|
||||
import { CreateCodeContainer } from './create-code-form';
|
||||
import classNames from 'classnames';
|
||||
import { Table } from './table';
|
||||
@@ -30,30 +28,25 @@ 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';
|
||||
|
||||
export const ReferralStatistics = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const program = useReferralProgram();
|
||||
|
||||
const { data: referee } = useReferral({
|
||||
pubKey,
|
||||
role: 'referee',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
const { data: referrer } = useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
|
||||
if (referee?.code) {
|
||||
return <Statistics data={referee} program={program} as="referee" />;
|
||||
return <Statistics data={referee} as="referee" />;
|
||||
}
|
||||
|
||||
if (referrer?.code) {
|
||||
return <Statistics data={referrer} program={program} as="referrer" />;
|
||||
return <Statistics data={referrer} as="referrer" />;
|
||||
}
|
||||
|
||||
return <CreateCodeContainer />;
|
||||
@@ -61,16 +54,14 @@ export const ReferralStatistics = () => {
|
||||
|
||||
export const Statistics = ({
|
||||
data,
|
||||
program,
|
||||
as,
|
||||
}: {
|
||||
data: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||
program: ReturnType<typeof useReferralProgram>;
|
||||
as: 'referrer' | 'referee';
|
||||
}) => {
|
||||
const { benefitTiers, details } = program;
|
||||
const { data: epochData } = useCurrentEpochInfoQuery();
|
||||
const { stakeAvailable } = useStakeAvailable();
|
||||
const { benefitTiers } = useReferralProgram();
|
||||
const { data: statsData } = useReferralSetStatsQuery({
|
||||
variables: {
|
||||
code: data.code,
|
||||
@@ -81,13 +72,6 @@ export const Statistics = ({
|
||||
|
||||
const currentEpoch = Number(epochData?.epoch.id);
|
||||
|
||||
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
});
|
||||
|
||||
const stats =
|
||||
statsData?.referralSetStats.edges &&
|
||||
compact(removePaginationWrapper(statsData.referralSetStats.edges));
|
||||
@@ -103,13 +87,10 @@ export const Statistics = ({
|
||||
const runningVolumeValue = statsAvailable
|
||||
? Number(statsAvailable.referralSetRunningNotionalTakerVolume)
|
||||
: 0;
|
||||
const referrerVolumeValue = statsAvailable
|
||||
? Number(statsAvailable.referrerTakerVolume)
|
||||
: 0;
|
||||
const multiplier = statsAvailable
|
||||
? Number(statsAvailable.rewardsMultiplier)
|
||||
: 1;
|
||||
const finalCommissionValue = isNaN(multiplier)
|
||||
const finalCommissionValue = !isNaN(multiplier)
|
||||
? baseCommissionValue
|
||||
: multiplier * baseCommissionValue;
|
||||
|
||||
@@ -137,13 +118,7 @@ export const Statistics = ({
|
||||
: 0;
|
||||
|
||||
const baseCommissionTile = (
|
||||
<StatTile
|
||||
title={t('Base commission rate')}
|
||||
description={t('(Combined set volume %s over last %s epochs)', [
|
||||
compactNumFormat.format(runningVolumeValue),
|
||||
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString(),
|
||||
])}
|
||||
>
|
||||
<StatTile title={t('Base commission rate')}>
|
||||
{baseCommissionValue * 100}%
|
||||
</StatTile>
|
||||
);
|
||||
@@ -159,16 +134,7 @@ export const Statistics = ({
|
||||
</StatTile>
|
||||
);
|
||||
const finalCommissionTile = (
|
||||
<StatTile
|
||||
title={t('Final commission rate')}
|
||||
description={
|
||||
!isNaN(multiplier)
|
||||
? `(${baseCommissionValue * 100}% ⨉ ${multiplier} = ${
|
||||
finalCommissionValue * 100
|
||||
}%)`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<StatTile title={t('Final commission rate')}>
|
||||
{finalCommissionValue * 100}%
|
||||
</StatTile>
|
||||
);
|
||||
@@ -177,21 +143,12 @@ export const Statistics = ({
|
||||
<StatTile title={t('Number of traders')}>{numberOfTradersValue}</StatTile>
|
||||
);
|
||||
|
||||
const codeTile = (
|
||||
<CodeTile
|
||||
code={data?.code}
|
||||
createdAt={getDateFormat().format(new Date(data.createdAt))}
|
||||
/>
|
||||
);
|
||||
|
||||
const referrerVolumeTile = (
|
||||
<StatTile
|
||||
title={t(
|
||||
'My volume (last %s epochs)',
|
||||
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString()
|
||||
)}
|
||||
>
|
||||
{compactNumFormat.format(referrerVolumeValue)}
|
||||
const codeTile = <CodeTile code={data?.code} />;
|
||||
const createdAtTile = (
|
||||
<StatTile title={t('Created at')}>
|
||||
<span className="text-3xl">
|
||||
{getDateFormat().format(new Date(data.createdAt))}
|
||||
</span>
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
@@ -200,11 +157,8 @@ export const Statistics = ({
|
||||
.reduce((all, r) => all.plus(r), new BigNumber(0));
|
||||
const totalCommissionTile = (
|
||||
<StatTile
|
||||
title={t(
|
||||
'Total commission (last %s epochs)',
|
||||
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString()
|
||||
)}
|
||||
description={<QUSDTooltip />}
|
||||
title={t('Total commission (last 30 days)')}
|
||||
description={t('(qUSD)')}
|
||||
>
|
||||
{getNumberFormat(0).format(Number(totalCommissionValue))}
|
||||
</StatTile>
|
||||
@@ -220,13 +174,20 @@ export const Statistics = ({
|
||||
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{codeTile}
|
||||
{referrerVolumeTile}
|
||||
{createdAtTile}
|
||||
{numberOfTradersTile}
|
||||
{totalCommissionTile}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
});
|
||||
|
||||
const currentBenefitTierTile = (
|
||||
<StatTile title={t('Current tier')}>
|
||||
{currentBenefitTierValue?.tier || 'None'}
|
||||
@@ -236,12 +197,7 @@ export const Statistics = ({
|
||||
<StatTile title={t('Discount')}>{discountFactorValue * 100}%</StatTile>
|
||||
);
|
||||
const runningVolumeTile = (
|
||||
<StatTile
|
||||
title={t(
|
||||
'Combined volume (last %s epochs)',
|
||||
details?.windowLength.toString()
|
||||
)}
|
||||
>
|
||||
<StatTile title={t('Combined volume')}>
|
||||
{compactNumFormat.format(runningVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
@@ -249,14 +205,24 @@ export const Statistics = ({
|
||||
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
|
||||
);
|
||||
const nextTierVolumeTile = (
|
||||
<StatTile title={t('Volume to next tier')}>
|
||||
<StatTile
|
||||
title={t(
|
||||
'Volume to next tier %s',
|
||||
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
|
||||
)}
|
||||
>
|
||||
{nextBenefitTierVolumeValue <= 0
|
||||
? '0'
|
||||
: compactNumFormat.format(nextBenefitTierVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
const nextTierEpochsTile = (
|
||||
<StatTile title={t('Epochs to next tier')}>
|
||||
<StatTile
|
||||
title={t(
|
||||
'Epochs to next tier %s',
|
||||
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
|
||||
)}
|
||||
>
|
||||
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
|
||||
</StatTile>
|
||||
);
|
||||
@@ -300,7 +266,7 @@ export const Statistics = ({
|
||||
{/* Referees (only for referrer view) */}
|
||||
{as === 'referrer' && data.referees.length > 0 && (
|
||||
<div className="mt-20 mb-20">
|
||||
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
|
||||
<h2 className="text-2xl mb-5">{t('Referees')}</h2>
|
||||
<div
|
||||
className={classNames(
|
||||
collapsed && [
|
||||
@@ -326,28 +292,10 @@ export const Statistics = ({
|
||||
columns={[
|
||||
{ name: 'party', displayName: t('Trader') },
|
||||
{ name: 'joined', displayName: t('Date Joined') },
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t(
|
||||
'Volume (last %s epochs)',
|
||||
(
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS
|
||||
).toString()
|
||||
),
|
||||
},
|
||||
{ name: 'volume', displayName: t('Volume (last 30 days)') },
|
||||
{
|
||||
name: 'commission',
|
||||
displayName: (
|
||||
<>
|
||||
{t('Commission earned in')} <QUSDTooltip />{' '}
|
||||
{t(
|
||||
'(last %s epochs)',
|
||||
(
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS
|
||||
).toString()
|
||||
)}
|
||||
</>
|
||||
),
|
||||
displayName: t('Commission earned (last 30 days)'),
|
||||
},
|
||||
]}
|
||||
data={sortBy(
|
||||
@@ -376,25 +324,3 @@ export const Statistics = ({
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const QUSDTooltip = () => (
|
||||
<Tooltip
|
||||
description={
|
||||
<>
|
||||
<p className="mb-1">
|
||||
{t(
|
||||
'qUSD provides a rough USD equivalent of balances across all assets using the value of "Quantum" for that asset'
|
||||
)}
|
||||
</p>
|
||||
{DocsLinks && (
|
||||
<ExternalLink href={DocsLinks.QUANTUM}>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
underline={true}
|
||||
>
|
||||
<span>{t('qUSD')}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { forwardRef, type ReactNode, type HTMLAttributes } from 'react';
|
||||
import { forwardRef, type HTMLAttributes } from 'react';
|
||||
import { BORDER_COLOR, GRADIENT } from './constants';
|
||||
|
||||
type TableColumnDefinition = {
|
||||
displayName?: ReactNode;
|
||||
displayName?: string;
|
||||
name: string;
|
||||
tooltip?: string;
|
||||
className?: string;
|
||||
@@ -46,7 +46,7 @@ export const Table = forwardRef<
|
||||
INNER_BORDER_STYLE
|
||||
)}
|
||||
>
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
<span className="flex flex-row gap-2 items-center">
|
||||
<span>{displayName}</span>
|
||||
{tooltip ? (
|
||||
<Tooltip description={tooltip}>
|
||||
@@ -102,14 +102,17 @@ export const Table = forwardRef<
|
||||
key={`${i}-${name}`}
|
||||
>
|
||||
{/** display column name in mobile view */}
|
||||
{!noCollapse && !noHeader && displayName && (
|
||||
<span
|
||||
aria-hidden
|
||||
className="px-0 font-mono text-xs md:hidden text-vega-clight-100 dark:text-vega-cdark-100"
|
||||
>
|
||||
{displayName}
|
||||
</span>
|
||||
)}
|
||||
{!noCollapse &&
|
||||
!noHeader &&
|
||||
displayName &&
|
||||
displayName.length > 0 && (
|
||||
<span
|
||||
aria-hidden
|
||||
className="md:hidden font-mono text-xs px-0 text-vega-clight-100 dark:text-vega-cdark-100"
|
||||
>
|
||||
{displayName}
|
||||
</span>
|
||||
)}
|
||||
<span>{d[name]}</span>
|
||||
</td>
|
||||
))}
|
||||
|
||||
@@ -109,7 +109,6 @@ export const TiersContainer = () => {
|
||||
<Loading variant="large" />
|
||||
) : (
|
||||
<TiersTable
|
||||
windowLength={details?.windowLength}
|
||||
data={benefitTiers.map((bt) => ({
|
||||
...bt,
|
||||
tierElement: (
|
||||
@@ -163,7 +162,6 @@ const StakingTiers = ({
|
||||
|
||||
const TiersTable = ({
|
||||
data,
|
||||
windowLength,
|
||||
}: {
|
||||
data: Array<{
|
||||
tier: number;
|
||||
@@ -172,7 +170,6 @@ const TiersTable = ({
|
||||
discount: string;
|
||||
volume: string;
|
||||
}>;
|
||||
windowLength?: number;
|
||||
}) => {
|
||||
return (
|
||||
<Table
|
||||
@@ -184,15 +181,7 @@ const TiersTable = ({
|
||||
tooltip: t('A percentage of commission earned by the referrer'),
|
||||
},
|
||||
{ name: 'discount', displayName: t('Referrer trading discount') },
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t(
|
||||
'Min. trading volume %s',
|
||||
windowLength
|
||||
? t('(last %s epochs)', windowLength.toString())
|
||||
: undefined
|
||||
),
|
||||
},
|
||||
{ name: 'volume', displayName: t('Min. trading volume') },
|
||||
{ name: 'epochs', displayName: t('Min. epochs') },
|
||||
]}
|
||||
data={data.map((d) => ({
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { Button } from './buttons';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export const Tile = ({
|
||||
className,
|
||||
@@ -29,7 +28,7 @@ export const Tile = ({
|
||||
|
||||
type StatTileProps = {
|
||||
title: string;
|
||||
description?: ReactNode;
|
||||
description?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
export const StatTile = ({ title, description, children }: StatTileProps) => {
|
||||
@@ -55,23 +54,18 @@ const FADE_OUT_STYLE = classNames(
|
||||
|
||||
export const CodeTile = ({
|
||||
code,
|
||||
createdAt,
|
||||
className,
|
||||
}: {
|
||||
code: string;
|
||||
createdAt?: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<StatTile
|
||||
title={t('Your referral code')}
|
||||
description={createdAt ? t('(Created at: %s)', createdAt) : undefined}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<StatTile title="Your referral code">
|
||||
<div className="flex gap-2 items-center justify-between">
|
||||
<Tooltip
|
||||
description={
|
||||
<div className="break-all">
|
||||
<span className="text-xl text-transparent bg-rainbow bg-clip-text">
|
||||
<span className="text-xl bg-rainbow bg-clip-text text-transparent">
|
||||
{code}
|
||||
</span>
|
||||
</div>
|
||||
@@ -88,7 +82,7 @@ export const CodeTile = ({
|
||||
</Tooltip>
|
||||
<CopyWithTooltip text={code}>
|
||||
<Button className="text-sm no-underline !py-0 !px-0 h-fit !bg-transparent">
|
||||
<span className="sr-only">{t('Copy')}</span>
|
||||
<span className="sr-only">Copy</span>
|
||||
<VegaIcon size={24} name={VegaIconNames.COPY} />
|
||||
</Button>
|
||||
</CopyWithTooltip>
|
||||
|
||||
@@ -10,28 +10,15 @@ import {
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
CopyWithTooltip,
|
||||
ExternalLink,
|
||||
Indicator,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Indicator } from '@vegaprotocol/ui-toolkit';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import {
|
||||
useCheckLiquidityStatus,
|
||||
usePaidFeesQuery,
|
||||
} from '@vegaprotocol/liquidity';
|
||||
import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
export const LiquidityHeader = () => {
|
||||
const { marketId } = useParams();
|
||||
const { data: market } = useMarket(marketId);
|
||||
const { data: marketData } = useStaticMarketData(marketId);
|
||||
const { data: feesPaidRes } = usePaidFeesQuery({
|
||||
variables: { marketId: marketId || '' },
|
||||
});
|
||||
const targetStake = marketData?.targetStake;
|
||||
const suppliedStake = marketData?.suppliedStake;
|
||||
|
||||
@@ -49,10 +36,6 @@ export const LiquidityHeader = () => {
|
||||
triggeringRatio,
|
||||
});
|
||||
|
||||
const feesObject = feesPaidRes?.paidLiquidityFees?.edges?.find(
|
||||
(e) => e?.node.marketId === marketId
|
||||
);
|
||||
|
||||
return (
|
||||
<Header
|
||||
title={
|
||||
@@ -99,40 +82,9 @@ export const LiquidityHeader = () => {
|
||||
<HeaderStat heading={t('Liquidity supplied')} testId="liquidity-supplied">
|
||||
<Indicator variant={status} /> {formatNumberPercentage(percentage, 2)}
|
||||
</HeaderStat>
|
||||
<HeaderStat
|
||||
heading={t('Fees paid')}
|
||||
description={t(
|
||||
'The amount of fees paid to liquidity providers across the whole market during the last epoch %s.',
|
||||
feesObject?.node.epoch.toString() || '-'
|
||||
)}
|
||||
testId="fees-paid"
|
||||
>
|
||||
<div>
|
||||
{feesObject?.node.totalFeesPaid
|
||||
? `${addDecimalsFormatNumber(
|
||||
feesObject?.node.totalFeesPaid,
|
||||
assetDecimalPlaces ?? 0
|
||||
)} ${symbol}`
|
||||
: '-'}
|
||||
</div>
|
||||
<HeaderStat heading={t('Market ID')} testId="liquidity-market-id">
|
||||
<div className="break-word">{marketId}</div>
|
||||
</HeaderStat>
|
||||
{marketId && (
|
||||
<HeaderStat heading={t('Market ID')} testId="liquidity-market-id">
|
||||
<div className="break-word">
|
||||
<CopyWithTooltip text={marketId}>
|
||||
<button
|
||||
data-testid="copy-eth-oracle-address"
|
||||
className="uppercase text-right"
|
||||
>
|
||||
<span className="flex gap-1">
|
||||
{truncateMiddle(marketId)}
|
||||
<VegaIcon name={VegaIconNames.COPY} size={16} />
|
||||
</span>
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
</HeaderStat>
|
||||
)}
|
||||
<HeaderStat heading={t('Learn more')} testId="liquidity-learn-more">
|
||||
{DocsLinks ? (
|
||||
<ExternalLink href={DocsLinks.LIQUIDITY}>
|
||||
|
||||
@@ -9,17 +9,12 @@ import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import type { Transfer } from '@vegaprotocol/wallet';
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/web3';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { accountsDataProvider } from './accounts-data-provider';
|
||||
import { TransferForm } from './transfer-form';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const ALLOWED_ACCOUNTS = [
|
||||
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
];
|
||||
|
||||
export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
const { pubKey, pubKeys } = useVegaWallet();
|
||||
const { param } = useNetworkParam(NetworkParams.transfer_fee_factor);
|
||||
@@ -38,19 +33,20 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
[create]
|
||||
);
|
||||
|
||||
const accounts = data
|
||||
? data.filter((account) => ALLOWED_ACCOUNTS.includes(account.type))
|
||||
: [];
|
||||
|
||||
const assets = accounts.map((account) => ({
|
||||
id: account.asset.id,
|
||||
symbol: account.asset.symbol,
|
||||
name: account.asset.name,
|
||||
decimals: account.asset.decimals,
|
||||
balance: addDecimal(account.balance, account.asset.decimals),
|
||||
}));
|
||||
|
||||
if (data === null) return null;
|
||||
const assets = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return data
|
||||
.filter(
|
||||
(account) => account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
|
||||
)
|
||||
.map((account) => ({
|
||||
id: account.asset.id,
|
||||
symbol: account.asset.symbol,
|
||||
name: account.asset.name,
|
||||
decimals: account.asset.decimals,
|
||||
balance: addDecimal(account.balance, account.asset.decimals),
|
||||
}));
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -73,7 +69,6 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
assetId={assetId}
|
||||
feeFactor={param}
|
||||
submitTransfer={transfer}
|
||||
accounts={accounts}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,38 +1,18 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { AddressField, TransferFee, TransferForm } from './transfer-form';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { addDecimal, formatNumber, removeDecimal } from '@vegaprotocol/utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('TransferForm', () => {
|
||||
const submit = async () => {
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', { name: 'Confirm transfer' })
|
||||
);
|
||||
};
|
||||
|
||||
const selectAsset = async (asset: {
|
||||
id: string;
|
||||
balance: string;
|
||||
name: string;
|
||||
decimals: number;
|
||||
}) => {
|
||||
// Bypass RichSelect and target hidden native select
|
||||
// eslint-disable-next-line
|
||||
fireEvent.change(document.querySelector('select[name="asset"]')!, {
|
||||
target: { value: asset.id },
|
||||
});
|
||||
|
||||
// assert rich select as updated
|
||||
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
||||
asset.name
|
||||
);
|
||||
expect(await screen.findByTestId('asset-balance')).toHaveTextContent(
|
||||
formatNumber(asset.balance, asset.decimals)
|
||||
);
|
||||
};
|
||||
|
||||
const submit = () => fireEvent.submit(screen.getByTestId('transfer-form'));
|
||||
const amount = '100';
|
||||
const pubKey =
|
||||
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
||||
@@ -52,18 +32,6 @@ describe('TransferForm', () => {
|
||||
assets: [asset],
|
||||
feeFactor: '0.001',
|
||||
submitTransfer: jest.fn(),
|
||||
accounts: [
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
asset,
|
||||
balance: '100',
|
||||
},
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
asset,
|
||||
balance: '100',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('form tooltips correctly displayed', async () => {
|
||||
@@ -74,45 +42,49 @@ describe('TransferForm', () => {
|
||||
// 1003-TRAN-019
|
||||
render(<TransferForm {...props} />);
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('Vega key'),
|
||||
props.pubKeys[1]
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: props.pubKeys[1] },
|
||||
});
|
||||
|
||||
// Select asset
|
||||
await selectAsset(asset);
|
||||
|
||||
fireEvent.change(
|
||||
// Bypass RichSelect and target hidden native select
|
||||
// eslint-disable-next-line
|
||||
document.querySelector('select[name="asset"]')!,
|
||||
{ target: { value: asset.id } }
|
||||
);
|
||||
// set valid amount
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
await userEvent.type(amountInput, amount);
|
||||
expect(amountInput).toHaveValue(amount);
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: amount },
|
||||
});
|
||||
|
||||
const includeTransferLabel = screen.getByText('Include transfer fee');
|
||||
await userEvent.hover(includeTransferLabel);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||
'The fee will be taken from the amount you are transferring.'
|
||||
);
|
||||
await userEvent.unhover(screen.getByText('Include transfer fee'));
|
||||
userEvent.hover(screen.getByText('Include transfer fee'));
|
||||
|
||||
const transferFee = screen.getByText('Transfer fee');
|
||||
await userEvent.hover(transferFee);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||
/transfer.fee.factor/
|
||||
);
|
||||
await userEvent.unhover(transferFee);
|
||||
await waitFor(() => {
|
||||
const tooltips = screen.getAllByTestId('tooltip-content');
|
||||
expect(tooltips[0]).toBeVisible();
|
||||
});
|
||||
|
||||
const amountToBeTransferred = screen.getByText('Amount to be transferred');
|
||||
await userEvent.hover(amountToBeTransferred);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||
/without the fee/
|
||||
);
|
||||
await userEvent.unhover(amountToBeTransferred);
|
||||
userEvent.hover(screen.getByText('Transfer fee'));
|
||||
|
||||
const totalAmountWithFee = screen.getByText('Total amount (with fee)');
|
||||
await userEvent.hover(totalAmountWithFee);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||
/total amount taken from your account/
|
||||
);
|
||||
await waitFor(() => {
|
||||
const tooltips = screen.getAllByTestId('tooltip-content');
|
||||
expect(tooltips[0]).toBeVisible();
|
||||
});
|
||||
|
||||
userEvent.hover(screen.getByText('Amount to be transferred'));
|
||||
|
||||
await waitFor(() => {
|
||||
const tooltips = screen.getAllByTestId('tooltip-content');
|
||||
expect(tooltips[0]).toBeVisible();
|
||||
});
|
||||
|
||||
userEvent.hover(screen.getByText('Total amount (with fee)'));
|
||||
|
||||
await waitFor(() => {
|
||||
const tooltips = screen.getAllByTestId('tooltip-content');
|
||||
expect(tooltips[0]).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
it('validates a manually entered address', async () => {
|
||||
@@ -120,17 +92,30 @@ describe('TransferForm', () => {
|
||||
// 1003-TRAN-013
|
||||
// 1003-TRAN-004
|
||||
render(<TransferForm {...props} />);
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(4);
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
const toggle = screen.getByText('Enter manually');
|
||||
await userEvent.click(toggle);
|
||||
fireEvent.click(toggle);
|
||||
// has switched to input
|
||||
expect(toggle).toHaveTextContent('Select from wallet');
|
||||
expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
|
||||
await userEvent.type(screen.getByLabelText('Vega key'), 'invalid-address');
|
||||
expect(screen.getAllByTestId('input-error-text')[0]).toHaveTextContent(
|
||||
'Invalid Vega key'
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: 'invalid-address' },
|
||||
});
|
||||
await waitFor(() => {
|
||||
const errors = screen.getAllByTestId('input-error-text');
|
||||
expect(errors[0]).toHaveTextContent('Invalid Vega key');
|
||||
});
|
||||
|
||||
// same pubkey
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: pubKey },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const errors = screen.getAllByTestId('input-error-text');
|
||||
expect(errors[0]).toHaveTextContent('Vega key is the same');
|
||||
});
|
||||
});
|
||||
|
||||
it('validates fields and submits', async () => {
|
||||
@@ -142,25 +127,28 @@ describe('TransferForm', () => {
|
||||
render(<TransferForm {...props} />);
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect = screen.getByLabelText<HTMLSelectElement>('Vega key');
|
||||
expect(keySelect.children).toHaveLength(3);
|
||||
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
|
||||
expect(keySelect.children).toHaveLength(2);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
|
||||
'',
|
||||
pubKey,
|
||||
props.pubKeys[1],
|
||||
]);
|
||||
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(4);
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('Vega key'),
|
||||
props.pubKeys[1]
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: props.pubKeys[1] },
|
||||
});
|
||||
|
||||
// Select asset
|
||||
await selectAsset(asset);
|
||||
fireEvent.change(
|
||||
// Bypass RichSelect and target hidden native select
|
||||
// eslint-disable-next-line
|
||||
document.querySelector('select[name="asset"]')!,
|
||||
{ target: { value: asset.id } }
|
||||
);
|
||||
|
||||
// assert rich select as updated
|
||||
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
||||
@@ -170,38 +158,37 @@ describe('TransferForm', () => {
|
||||
formatNumber(asset.balance, asset.decimals)
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
// Test amount validation
|
||||
await userEvent.type(amountInput, '0.00000001');
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: '0.00000001' },
|
||||
});
|
||||
expect(
|
||||
await screen.findByText('Value is below minimum')
|
||||
).toBeInTheDocument();
|
||||
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, '9999999');
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: '9999999' },
|
||||
});
|
||||
expect(
|
||||
await screen.findByText(/cannot transfer more/i)
|
||||
).toBeInTheDocument();
|
||||
|
||||
// set valid amount
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, amount);
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: amount },
|
||||
});
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
|
||||
new BigNumber(props.feeFactor).times(amount).toFixed()
|
||||
);
|
||||
|
||||
await submit();
|
||||
submit();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
|
||||
expect(props.submitTransfer).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKeys[1],
|
||||
asset: asset.id,
|
||||
@@ -213,50 +200,59 @@ describe('TransferForm', () => {
|
||||
|
||||
describe('IncludeFeesCheckbox', () => {
|
||||
it('validates fields and submits when checkbox is checked', async () => {
|
||||
const mockSubmit = jest.fn();
|
||||
render(<TransferForm {...props} submitTransfer={mockSubmit} />);
|
||||
render(<TransferForm {...props} />);
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect = screen.getByLabelText<HTMLSelectElement>('Vega key');
|
||||
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
|
||||
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
|
||||
pubKeyOptions
|
||||
);
|
||||
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
|
||||
expect(keySelect.children).toHaveLength(2);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
|
||||
'',
|
||||
props.pubKeys[1],
|
||||
]);
|
||||
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(4);
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('Vega key'),
|
||||
props.pubKeys[1]
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: props.pubKeys[1] },
|
||||
});
|
||||
|
||||
// Select asset
|
||||
await selectAsset(asset);
|
||||
fireEvent.change(
|
||||
// Bypass RichSelect and target hidden native select
|
||||
// eslint-disable-next-line
|
||||
document.querySelector('select[name="asset"]')!,
|
||||
{ target: { value: asset.id } }
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||
// assert rich select as updated
|
||||
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
||||
asset.name
|
||||
);
|
||||
expect(await screen.findByTestId('asset-balance')).toHaveTextContent(
|
||||
formatNumber(asset.balance, asset.decimals)
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
|
||||
// 1003-TRAN-022
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, amount);
|
||||
await userEvent.click(checkbox);
|
||||
act(() => {
|
||||
/* fire events that update state */
|
||||
// set valid amount
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: amount },
|
||||
});
|
||||
// check include fees checkbox
|
||||
fireEvent.click(checkbox);
|
||||
});
|
||||
|
||||
expect(checkbox).toBeChecked();
|
||||
const expectedFee = new BigNumber(amount)
|
||||
.times(props.feeFactor)
|
||||
.toFixed();
|
||||
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
|
||||
|
||||
// 1003-TRAN-020
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
|
||||
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
|
||||
@@ -266,17 +262,18 @@ describe('TransferForm', () => {
|
||||
amount
|
||||
);
|
||||
|
||||
await submit();
|
||||
submit();
|
||||
|
||||
await waitFor(() => {
|
||||
// 1003-TRAN-023
|
||||
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockSubmit).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
|
||||
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
|
||||
expect(props.submitTransfer).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKeys[1],
|
||||
asset: asset.id,
|
||||
amount: removeDecimal(expectedAmount, asset.decimals),
|
||||
amount: removeDecimal(amount, asset.decimals),
|
||||
oneOff: {},
|
||||
});
|
||||
});
|
||||
@@ -287,29 +284,46 @@ describe('TransferForm', () => {
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
|
||||
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
|
||||
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
|
||||
pubKeyOptions
|
||||
);
|
||||
expect(keySelect.children).toHaveLength(2);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
|
||||
'',
|
||||
props.pubKeys[1],
|
||||
]);
|
||||
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(4);
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('Vega key'),
|
||||
props.pubKeys[1]
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: props.pubKeys[1] },
|
||||
});
|
||||
|
||||
// Select asset
|
||||
await selectAsset(asset);
|
||||
fireEvent.change(
|
||||
// Bypass RichSelect and target hidden native select
|
||||
// eslint-disable-next-line
|
||||
document.querySelector('select[name="asset"]')!,
|
||||
{ target: { value: asset.id } }
|
||||
);
|
||||
|
||||
// assert rich select as updated
|
||||
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
||||
asset.name
|
||||
);
|
||||
expect(await screen.findByTestId('asset-balance')).toHaveTextContent(
|
||||
formatNumber(asset.balance, asset.decimals)
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
await userEvent.type(amountInput, amount);
|
||||
act(() => {
|
||||
/* fire events that update state */
|
||||
// set valid amount
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: amount },
|
||||
});
|
||||
});
|
||||
expect(checkbox).not.toBeChecked();
|
||||
const expectedFee = new BigNumber(amount)
|
||||
.times(props.feeFactor)
|
||||
@@ -337,11 +351,11 @@ describe('TransferForm', () => {
|
||||
// select should be shown as multiple pubkeys provided
|
||||
expect(screen.getByText('select')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('Enter manually'));
|
||||
fireEvent.click(screen.getByText('Enter manually'));
|
||||
expect(screen.queryByText('select')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('input')).toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||
await userEvent.click(screen.getByText('Select from wallet'));
|
||||
fireEvent.click(screen.getByText('Select from wallet'));
|
||||
expect(screen.getByText('select')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -24,30 +24,22 @@ import type { ReactNode } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { AssetOption, Balance } from '@vegaprotocol/assets';
|
||||
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
|
||||
|
||||
interface FormFields {
|
||||
toAddress: string;
|
||||
asset: string;
|
||||
amount: string;
|
||||
fromAccount: AccountType;
|
||||
}
|
||||
|
||||
interface Asset {
|
||||
id: string;
|
||||
symbol: string;
|
||||
name: string;
|
||||
decimals: number;
|
||||
balance: string;
|
||||
}
|
||||
|
||||
interface TransferFormProps {
|
||||
pubKey: string | null;
|
||||
pubKeys: string[] | null;
|
||||
assets: Array<Asset>;
|
||||
accounts: Array<{
|
||||
type: AccountType;
|
||||
asset: { id: string; symbol: string };
|
||||
assets: Array<{
|
||||
id: string;
|
||||
symbol: string;
|
||||
name: string;
|
||||
decimals: number;
|
||||
balance: string;
|
||||
}>;
|
||||
assetId?: string;
|
||||
feeFactor: string | null;
|
||||
@@ -61,7 +53,6 @@ export const TransferForm = ({
|
||||
assetId: initialAssetId,
|
||||
feeFactor,
|
||||
submitTransfer,
|
||||
accounts,
|
||||
}: TransferFormProps) => {
|
||||
const {
|
||||
control,
|
||||
@@ -76,7 +67,6 @@ export const TransferForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const selectedPubKey = watch('toAddress');
|
||||
const amount = watch('amount');
|
||||
const assetId = watch('asset');
|
||||
|
||||
@@ -112,16 +102,10 @@ export const TransferForm = ({
|
||||
if (!transferAmount) {
|
||||
throw new Error('Submitted transfer with no amount selected');
|
||||
}
|
||||
const transfer = normalizeTransfer(
|
||||
fields.toAddress,
|
||||
transferAmount,
|
||||
fields.fromAccount,
|
||||
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
|
||||
{
|
||||
id: asset.id,
|
||||
decimals: asset.decimals,
|
||||
}
|
||||
);
|
||||
const transfer = normalizeTransfer(fields.toAddress, transferAmount, {
|
||||
id: asset.id,
|
||||
decimals: asset.decimals,
|
||||
});
|
||||
submitTransfer(transfer);
|
||||
},
|
||||
[asset, submitTransfer, transferAmount]
|
||||
@@ -153,53 +137,57 @@ export const TransferForm = ({
|
||||
className="text-sm"
|
||||
data-testid="transfer-form"
|
||||
>
|
||||
<TradingFormGroup label="Vega key" labelFor="toAddress">
|
||||
<TradingFormGroup label="Vega key" labelFor="to-address">
|
||||
<AddressField
|
||||
pubKeys={pubKeys}
|
||||
onChange={() => setValue('toAddress', '')}
|
||||
select={
|
||||
<TradingSelect
|
||||
{...register('toAddress')}
|
||||
id="toAddress"
|
||||
id="to-address"
|
||||
defaultValue=""
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{pubKeys?.length &&
|
||||
pubKeys.map((pk) => {
|
||||
const text = pk === pubKey ? t('Current key: ') + pk : pk;
|
||||
|
||||
return (
|
||||
pubKeys
|
||||
.filter((pk) => pk !== pubKey) // remove currently selected pubkey
|
||||
.map((pk) => (
|
||||
<option key={pk} value={pk}>
|
||||
{text}
|
||||
{pk}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</TradingSelect>
|
||||
}
|
||||
input={
|
||||
<TradingInput
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true} // focus input immediately after is shown
|
||||
id="toAddress"
|
||||
id="to-address"
|
||||
type="text"
|
||||
{...register('toAddress', {
|
||||
validate: {
|
||||
required,
|
||||
vegaPublicKey,
|
||||
sameKey: (value) => {
|
||||
if (value === pubKey) {
|
||||
return t('Vega key is the same as current key');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{errors.toAddress?.message && (
|
||||
<TradingInputError forInput="toAddress">
|
||||
<TradingInputError forInput="to-address">
|
||||
{errors.toAddress.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label={t('Asset')} labelFor="asset">
|
||||
<TradingFormGroup label="Asset" labelFor="asset">
|
||||
<Controller
|
||||
control={control}
|
||||
name="asset"
|
||||
@@ -240,63 +228,6 @@ export const TransferForm = ({
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label={t('From account')} labelFor="fromAccount">
|
||||
<TradingSelect
|
||||
id="fromAccount"
|
||||
defaultValue=""
|
||||
{...register('fromAccount', {
|
||||
validate: {
|
||||
required,
|
||||
sameAccount: (value) => {
|
||||
if (
|
||||
pubKey === selectedPubKey &&
|
||||
value === AccountType.ACCOUNT_TYPE_GENERAL
|
||||
) {
|
||||
return t(
|
||||
'Cannot transfer to the same account type for the connected key'
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
})}
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{accounts
|
||||
.filter((a) => {
|
||||
if (!assetId) return true;
|
||||
return assetId === a.asset.id;
|
||||
})
|
||||
.map((a) => {
|
||||
return (
|
||||
<option value={a.type} key={`${a.type}-${a.asset.id}`}>
|
||||
{AccountTypeMapping[a.type]} ({a.asset.symbol})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
{errors.fromAccount?.message && (
|
||||
<TradingInputError forInput="fromAccount">
|
||||
{errors.fromAccount.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label={t('To account')} labelFor="toAccount">
|
||||
<TradingSelect
|
||||
id="toAccount"
|
||||
defaultValue={AccountType.ACCOUNT_TYPE_GENERAL}
|
||||
>
|
||||
<option value={AccountType.ACCOUNT_TYPE_GENERAL}>
|
||||
{asset
|
||||
? `${AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]} (${
|
||||
asset.symbol
|
||||
})`
|
||||
: AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]}
|
||||
</option>
|
||||
</TradingSelect>
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label="Amount" labelFor="amount">
|
||||
<TradingInput
|
||||
id="amount"
|
||||
@@ -460,7 +391,7 @@ export const AddressField = ({
|
||||
setIsInput((curr) => !curr);
|
||||
onChange();
|
||||
}}
|
||||
className="absolute top-0 right-0 ml-auto text-xs underline"
|
||||
className="absolute top-0 right-0 ml-auto text-sm underline"
|
||||
>
|
||||
{isInput ? t('Select from wallet') : t('Enter manually')}
|
||||
</button>
|
||||
|
||||
@@ -23,4 +23,3 @@ export * from './lib/type-helpers';
|
||||
export * from './lib/cells/grid-progress-bar';
|
||||
|
||||
export * from './lib/use-datagrid-events';
|
||||
export * from './lib/pagination';
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Pagination } from './pagination';
|
||||
|
||||
describe('Pagination', () => {
|
||||
const props = {
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
},
|
||||
count: 0,
|
||||
onLoad: () => undefined,
|
||||
showRetentionMessage: false,
|
||||
hasDisplayedRows: false,
|
||||
};
|
||||
|
||||
it('renders message for 0 rows', async () => {
|
||||
const mockOnLoad = jest.fn();
|
||||
render(<Pagination {...props} onLoad={mockOnLoad} />);
|
||||
expect(screen.getByText('0 rows loaded')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
|
||||
expect(mockOnLoad).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders mesasge for multiple rows', async () => {
|
||||
const mockOnLoad = jest.fn();
|
||||
const count = 10;
|
||||
render(<Pagination {...props} count={count} onLoad={mockOnLoad} />);
|
||||
expect(screen.getByText(`${count} rows loaded`)).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
|
||||
expect(mockOnLoad).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders message for a single row', async () => {
|
||||
const mockOnLoad = jest.fn();
|
||||
const count = 1;
|
||||
render(<Pagination {...props} count={count} onLoad={mockOnLoad} />);
|
||||
expect(screen.getByText(`${count} row loaded`)).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
|
||||
expect(mockOnLoad).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders the data rentention message', () => {
|
||||
render(<Pagination {...props} showRetentionMessage={true} />);
|
||||
expect(screen.getByText(/data node retention/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the row filter message', () => {
|
||||
render(<Pagination {...props} count={1} hasDisplayedRows={false} />);
|
||||
expect(screen.getByText(/No rows matching/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export const Pagination = ({
|
||||
count,
|
||||
pageInfo,
|
||||
onLoad,
|
||||
hasDisplayedRows,
|
||||
showRetentionMessage,
|
||||
}: {
|
||||
count: number;
|
||||
pageInfo: { hasNextPage?: boolean } | null;
|
||||
onLoad: () => void;
|
||||
hasDisplayedRows: boolean;
|
||||
showRetentionMessage: boolean;
|
||||
}) => {
|
||||
let rowMessage = '';
|
||||
|
||||
if (count && !pageInfo?.hasNextPage) {
|
||||
rowMessage = t('all %s rows loaded', count.toString());
|
||||
} else {
|
||||
if (count === 1) {
|
||||
rowMessage = t('%s row loaded', count.toString());
|
||||
} else {
|
||||
rowMessage = t('%s rows loaded', count.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-1 border-t border-default">
|
||||
<div className="text-xs">
|
||||
{false}
|
||||
{showRetentionMessage &&
|
||||
t(
|
||||
'Depending on data node retention you may not be able see the "full" history'
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center text-xs">
|
||||
<span>{rowMessage}</span>
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button size="extra-small" className="ml-1" onClick={onLoad}>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{count && hasDisplayedRows === false ? (
|
||||
<div className="absolute text-xs top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||
{t('No rows matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -26,7 +26,11 @@ import {
|
||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
} from '../../constants';
|
||||
import { useEstimateFees } from '../../hooks/use-estimate-fees';
|
||||
import {
|
||||
sumFees,
|
||||
sumFeesDiscounts,
|
||||
useEstimateFees,
|
||||
} from '../../hooks/use-estimate-fees';
|
||||
import { KeyValue } from './key-value';
|
||||
import {
|
||||
Accordion,
|
||||
@@ -40,7 +44,6 @@ import {
|
||||
import classNames from 'classnames';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { FeesBreakdown } from '../fees-breakdown';
|
||||
import { getTotalDiscountFactor, getDiscountedFee } from '../discounts';
|
||||
|
||||
const emptyValue = '-';
|
||||
|
||||
@@ -60,55 +63,54 @@ export const DealTicketFeeDetails = ({
|
||||
const feeEstimate = useEstimateFees(order, isMarketInAuction);
|
||||
const asset = getAsset(market);
|
||||
const { decimals: assetDecimals, quantum } = asset;
|
||||
const totalFees = feeEstimate?.fees && sumFees(feeEstimate?.fees);
|
||||
const feesDiscounts =
|
||||
feeEstimate?.fees && sumFeesDiscounts(feeEstimate?.fees);
|
||||
|
||||
const totalDiscountFactor = getTotalDiscountFactor(feeEstimate);
|
||||
const totalDiscountedFeeAmount =
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
getDiscountedFee(
|
||||
feeEstimate.totalFeeAmount,
|
||||
feeEstimate.referralDiscountFactor,
|
||||
feeEstimate.volumeDiscountFactor
|
||||
).discountedFee;
|
||||
|
||||
const totalPercentageDiscount =
|
||||
feesDiscounts &&
|
||||
totalFees &&
|
||||
feesDiscounts.total !== '0' &&
|
||||
totalFees !== '0' &&
|
||||
new BigNumber(feesDiscounts.total)
|
||||
.dividedBy(BigNumber.sum(totalFees, feesDiscounts.total))
|
||||
.times(100);
|
||||
return (
|
||||
<KeyValue
|
||||
label={t('Fees')}
|
||||
value={
|
||||
totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals)}`
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
|
||||
}
|
||||
formattedValue={
|
||||
<>
|
||||
{totalDiscountFactor ? (
|
||||
{totalPercentageDiscount && (
|
||||
<Pill size="xxs" intent={Intent.Warning} className="mr-1">
|
||||
-
|
||||
{formatNumberPercentage(
|
||||
new BigNumber(totalDiscountFactor).multipliedBy(100),
|
||||
2
|
||||
)}
|
||||
-{formatNumberPercentage(totalPercentageDiscount, 2)}
|
||||
</Pill>
|
||||
) : null}
|
||||
{totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`}
|
||||
)}
|
||||
{feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(
|
||||
feeEstimate?.totalFeeAmount,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}`}
|
||||
</>
|
||||
}
|
||||
labelDescription={
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>
|
||||
<>
|
||||
<span>
|
||||
{t(
|
||||
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}. Fees estimated are "taker" fees and will only be payable if the order trades aggressively. Rebate equal to the maker portion will be paid to the trader if the order trades passively.`
|
||||
)}
|
||||
</p>
|
||||
</span>
|
||||
<FeesBreakdown
|
||||
totalFeeAmount={feeEstimate?.totalFeeAmount}
|
||||
referralDiscountFactor={feeEstimate?.referralDiscountFactor}
|
||||
volumeDiscountFactor={feeEstimate?.volumeDiscountFactor}
|
||||
fees={feeEstimate?.fees}
|
||||
feeFactors={market.fees.factors}
|
||||
symbol={assetSymbol}
|
||||
decimals={assetDecimals}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
symbol={assetSymbol}
|
||||
/>
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { getDiscountedFee, getTotalDiscountFactor } from './discounts';
|
||||
|
||||
describe('getDiscountedFee', () => {
|
||||
it('calculates values if volumeDiscount or referralDiscount is undefined', () => {
|
||||
expect(getDiscountedFee('100')).toEqual({
|
||||
discountedFee: '100',
|
||||
volumeDiscount: '0',
|
||||
referralDiscount: '0',
|
||||
});
|
||||
expect(getDiscountedFee('100', undefined, '0.1')).toEqual({
|
||||
discountedFee: '90',
|
||||
volumeDiscount: '10',
|
||||
referralDiscount: '0',
|
||||
});
|
||||
expect(getDiscountedFee('100', '0.1', undefined)).toEqual({
|
||||
discountedFee: '90',
|
||||
volumeDiscount: '0',
|
||||
referralDiscount: '10',
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates values using volumeDiscount or referralDiscount', () => {
|
||||
expect(getDiscountedFee('', '0.1', '0.2')).toEqual({
|
||||
discountedFee: '',
|
||||
volumeDiscount: '0',
|
||||
referralDiscount: '0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTotalDiscountFactor', () => {
|
||||
it('returns 0 if discounts are 0', () => {
|
||||
expect(
|
||||
getTotalDiscountFactor({
|
||||
volumeDiscountFactor: '0',
|
||||
referralDiscountFactor: '0',
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it('returns volumeDiscountFactor if referralDiscountFactor is 0', () => {
|
||||
expect(
|
||||
getTotalDiscountFactor({
|
||||
volumeDiscountFactor: '0.1',
|
||||
referralDiscountFactor: '0',
|
||||
})
|
||||
).toEqual(0.1);
|
||||
});
|
||||
it('returns referralDiscountFactor if volumeDiscountFactor is 0', () => {
|
||||
expect(
|
||||
getTotalDiscountFactor({
|
||||
volumeDiscountFactor: '0',
|
||||
referralDiscountFactor: '0.1',
|
||||
})
|
||||
).toEqual(0.1);
|
||||
});
|
||||
|
||||
it('calculates discount using referralDiscountFactor and volumeDiscountFactor', () => {
|
||||
expect(
|
||||
getTotalDiscountFactor({
|
||||
volumeDiscountFactor: '0.2',
|
||||
referralDiscountFactor: '0.1',
|
||||
})
|
||||
).toBeCloseTo(0.28);
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export const getDiscountedFee = (
|
||||
feeAmount: string,
|
||||
referralDiscountFactor?: string,
|
||||
volumeDiscountFactor?: string
|
||||
) => {
|
||||
if (
|
||||
((!referralDiscountFactor || referralDiscountFactor === '0') &&
|
||||
(!volumeDiscountFactor || volumeDiscountFactor === '0')) ||
|
||||
!feeAmount ||
|
||||
feeAmount === '0'
|
||||
) {
|
||||
return {
|
||||
discountedFee: feeAmount,
|
||||
volumeDiscount: '0',
|
||||
referralDiscount: '0',
|
||||
};
|
||||
}
|
||||
const referralDiscount = new BigNumber(referralDiscountFactor || '0')
|
||||
.multipliedBy(feeAmount)
|
||||
.toFixed(0, BigNumber.ROUND_FLOOR);
|
||||
const volumeDiscount = new BigNumber(volumeDiscountFactor || '0')
|
||||
.multipliedBy((BigInt(feeAmount) - BigInt(referralDiscount)).toString())
|
||||
.toFixed(0, BigNumber.ROUND_FLOOR);
|
||||
const discountedFee = (
|
||||
BigInt(feeAmount || '0') -
|
||||
BigInt(referralDiscount) -
|
||||
BigInt(volumeDiscount)
|
||||
).toString();
|
||||
return {
|
||||
referralDiscount,
|
||||
volumeDiscount,
|
||||
discountedFee,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTotalDiscountFactor = (feeEstimate?: {
|
||||
volumeDiscountFactor?: string;
|
||||
referralDiscountFactor?: string;
|
||||
}) => {
|
||||
if (!feeEstimate) {
|
||||
return 0;
|
||||
}
|
||||
const volumeFactor = Number(feeEstimate?.volumeDiscountFactor) || 0;
|
||||
const referralFactor = Number(feeEstimate?.referralDiscountFactor) || 0;
|
||||
if (!volumeFactor) {
|
||||
return referralFactor;
|
||||
}
|
||||
if (!referralFactor) {
|
||||
return volumeFactor;
|
||||
}
|
||||
return 1 - (1 - volumeFactor) * (1 - referralFactor);
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { FeesBreakdown } from './fees-breakdown';
|
||||
|
||||
describe('FeesBreakdown', () => {
|
||||
it('formats fee factors correctly', () => {
|
||||
const feeFactors = {
|
||||
makerFee: '0.00005',
|
||||
infrastructureFee: '0.001',
|
||||
liquidityFee: '0.5',
|
||||
};
|
||||
const fees = {
|
||||
makerFee: '100',
|
||||
infrastructureFee: '100',
|
||||
liquidityFee: '100',
|
||||
};
|
||||
const props = {
|
||||
totalFeeAmount: '100',
|
||||
fees,
|
||||
feeFactors,
|
||||
symbol: 'USD',
|
||||
decimals: 2,
|
||||
referralDiscountFactor: '0.01',
|
||||
volumeDiscountFactor: '0.01',
|
||||
};
|
||||
render(<FeesBreakdown {...props} />);
|
||||
expect(screen.getByText('Maker fee').nextElementSibling).toHaveTextContent(
|
||||
'0.005%'
|
||||
);
|
||||
expect(
|
||||
screen.getByText('Infrastructure fee').nextElementSibling
|
||||
).toHaveTextContent('0.1%');
|
||||
expect(
|
||||
screen.getByText('Liquidity fee').nextElementSibling
|
||||
).toHaveTextContent('50%');
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { getDiscountedFee } from '../discounts';
|
||||
import { sumFees, sumFeesDiscounts } from '../../hooks';
|
||||
|
||||
const formatValue = (
|
||||
value: string | number | null | undefined,
|
||||
@@ -24,7 +24,7 @@ const FeesBreakdownItem = ({
|
||||
decimals,
|
||||
}: {
|
||||
label: string;
|
||||
factor?: string;
|
||||
factor?: BigNumber;
|
||||
value: string;
|
||||
symbol?: string;
|
||||
decimals: number;
|
||||
@@ -33,7 +33,7 @@ const FeesBreakdownItem = ({
|
||||
<dt className="col-span-2">{label}</dt>
|
||||
{factor && (
|
||||
<dd className="text-right col-span-1">
|
||||
{formatNumberPercentage(new BigNumber(factor).times(100))}
|
||||
{formatNumberPercentage(new BigNumber(factor).times(100), 2)}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-3">
|
||||
@@ -43,86 +43,76 @@ const FeesBreakdownItem = ({
|
||||
);
|
||||
|
||||
export const FeesBreakdown = ({
|
||||
totalFeeAmount,
|
||||
fees,
|
||||
feeFactors,
|
||||
symbol,
|
||||
decimals,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor,
|
||||
}: {
|
||||
totalFeeAmount?: string;
|
||||
fees?: TradeFee;
|
||||
feeFactors?: FeeFactors;
|
||||
symbol?: string;
|
||||
decimals: number;
|
||||
referralDiscountFactor?: string;
|
||||
volumeDiscountFactor?: string;
|
||||
}) => {
|
||||
if (!fees || !totalFeeAmount || totalFeeAmount === '0') return null;
|
||||
|
||||
const { discountedFee: discountedInfrastructureFee } = getDiscountedFee(
|
||||
fees.infrastructureFee,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
const { discountedFee: discountedLiquidityFee } = getDiscountedFee(
|
||||
fees.liquidityFee,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
const { discountedFee: discountedMakerFee } = getDiscountedFee(
|
||||
fees.makerFee,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
const { volumeDiscount, referralDiscount } = getDiscountedFee(
|
||||
totalFeeAmount,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
if (!fees) return null;
|
||||
const totalFees = sumFees(fees);
|
||||
const {
|
||||
total: totalDiscount,
|
||||
referral: referralDiscount,
|
||||
volume: volumeDiscount,
|
||||
} = sumFeesDiscounts(fees);
|
||||
if (totalFees === '0') return null;
|
||||
return (
|
||||
<dl className="grid grid-cols-6">
|
||||
<FeesBreakdownItem
|
||||
label={t('Infrastructure fee')}
|
||||
factor={feeFactors?.infrastructureFee}
|
||||
value={discountedInfrastructureFee}
|
||||
factor={
|
||||
feeFactors?.infrastructureFee
|
||||
? new BigNumber(feeFactors?.infrastructureFee)
|
||||
: undefined
|
||||
}
|
||||
value={fees.infrastructureFee}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
|
||||
<FeesBreakdownItem
|
||||
label={t('Liquidity fee')}
|
||||
factor={feeFactors?.liquidityFee}
|
||||
value={discountedLiquidityFee}
|
||||
factor={
|
||||
feeFactors?.liquidityFee
|
||||
? new BigNumber(feeFactors?.liquidityFee)
|
||||
: undefined
|
||||
}
|
||||
value={fees.liquidityFee}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
|
||||
<FeesBreakdownItem
|
||||
label={t('Maker fee')}
|
||||
factor={feeFactors?.makerFee}
|
||||
value={discountedMakerFee}
|
||||
factor={
|
||||
feeFactors?.makerFee ? new BigNumber(feeFactors?.makerFee) : undefined
|
||||
}
|
||||
value={fees.makerFee}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
{volumeDiscountFactor && volumeDiscount !== '0' && (
|
||||
{volumeDiscount && volumeDiscount !== '0' && (
|
||||
<FeesBreakdownItem
|
||||
label={t('Volume discount')}
|
||||
factor={volumeDiscountFactor}
|
||||
factor={new BigNumber(volumeDiscount).dividedBy(
|
||||
BigNumber.sum(totalFees, totalDiscount)
|
||||
)}
|
||||
value={volumeDiscount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
)}
|
||||
{referralDiscountFactor && referralDiscount !== '0' && (
|
||||
{referralDiscount && referralDiscount !== '0' && (
|
||||
<FeesBreakdownItem
|
||||
label={t('Referral discount')}
|
||||
factor={referralDiscountFactor}
|
||||
factor={new BigNumber(referralDiscount).dividedBy(
|
||||
BigNumber.sum(totalFees, totalDiscount)
|
||||
)}
|
||||
value={referralDiscount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
@@ -130,8 +120,8 @@ export const FeesBreakdown = ({
|
||||
)}
|
||||
<FeesBreakdownItem
|
||||
label={t('Total fees')}
|
||||
factor={feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined}
|
||||
value={totalFeeAmount}
|
||||
factor={feeFactors ? sumFeesFactors(feeFactors) : undefined}
|
||||
value={totalFees}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
|
||||
@@ -31,25 +31,4 @@ query EstimateFees(
|
||||
}
|
||||
totalFeeAmount
|
||||
}
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
runningVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(partyId: $partyId, pagination: { last: 1 }) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
referralSetRunningNotionalTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-22
@@ -15,7 +15,7 @@ export type EstimateFeesQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type EstimateFeesQuery = { __typename?: 'Query', estimateFees: { __typename?: 'FeeEstimate', totalFeeAmount: string, fees: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } }, epoch: { __typename?: 'Epoch', id: string }, volumeDiscountStats: { __typename?: 'VolumeDiscountStatsConnection', edges: Array<{ __typename?: 'VolumeDiscountStatsEdge', node: { __typename?: 'VolumeDiscountStats', atEpoch: number, discountFactor: string, runningVolume: string } } | null> }, referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, discountFactor: string, referralSetRunningNotionalTakerVolume: string } } | null> } };
|
||||
export type EstimateFeesQuery = { __typename?: 'Query', estimateFees: { __typename?: 'FeeEstimate', totalFeeAmount: string, fees: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } } };
|
||||
|
||||
|
||||
export const EstimateFeesDocument = gql`
|
||||
@@ -43,27 +43,6 @@ export const EstimateFeesDocument = gql`
|
||||
}
|
||||
totalFeeAmount
|
||||
}
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
runningVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(partyId: $partyId, pagination: {last: 1}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
referralSetRunningNotionalTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -6,15 +6,6 @@ export const estimateFeesQuery = (
|
||||
override?: PartialDeep<EstimateFeesQuery>
|
||||
): EstimateFeesQuery => {
|
||||
const defaultResult: EstimateFeesQuery = {
|
||||
epoch: {
|
||||
id: '1',
|
||||
},
|
||||
referralSetStats: {
|
||||
edges: [],
|
||||
},
|
||||
volumeDiscountStats: {
|
||||
edges: [],
|
||||
},
|
||||
estimateFees: {
|
||||
__typename: 'FeeEstimate',
|
||||
totalFeeAmount: '0.0006',
|
||||
|
||||
@@ -5,31 +5,6 @@ import { Side, OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||
import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
|
||||
|
||||
const data: EstimateFeesQuery = {
|
||||
epoch: {
|
||||
id: '3',
|
||||
},
|
||||
volumeDiscountStats: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 1,
|
||||
discountFactor: '0.1',
|
||||
runningVolume: '100',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
referralSetStats: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 1,
|
||||
discountFactor: '0.2',
|
||||
referralSetRunningNotionalTakerVolume: '100',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
estimateFees: {
|
||||
totalFeeAmount: '120',
|
||||
fees: {
|
||||
@@ -79,8 +54,6 @@ describe('useEstimateFees', () => {
|
||||
liquidityFee: '0',
|
||||
makerFee: '0',
|
||||
},
|
||||
referralDiscountFactor: '0',
|
||||
volumeDiscountFactor: '0',
|
||||
});
|
||||
expect(mockUseEstimateFeesQuery.mock.lastCall?.[0].skip).toBeTruthy();
|
||||
});
|
||||
@@ -112,46 +85,6 @@ describe('useEstimateFees', () => {
|
||||
makerFeeReferralDiscount: '5',
|
||||
makerFeeVolumeDiscount: '6',
|
||||
},
|
||||
referralDiscountFactor: '0',
|
||||
volumeDiscountFactor: '0',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns 0 discounts if discount stats are not at the current epoch -1', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useEstimateFees(
|
||||
{
|
||||
marketId: 'marketId',
|
||||
side: Side.SIDE_BUY,
|
||||
size: '1',
|
||||
price: '1',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
},
|
||||
true
|
||||
)
|
||||
);
|
||||
expect(result.current?.referralDiscountFactor).toEqual('0');
|
||||
expect(result.current?.volumeDiscountFactor).toEqual('0');
|
||||
});
|
||||
|
||||
it('returns discounts', () => {
|
||||
data.epoch.id = '2';
|
||||
const { result } = renderHook(() =>
|
||||
useEstimateFees(
|
||||
{
|
||||
marketId: 'marketId',
|
||||
side: Side.SIDE_BUY,
|
||||
size: '1',
|
||||
price: '1',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
},
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
expect(result.current?.referralDiscountFactor).toEqual('0.2');
|
||||
expect(result.current?.volumeDiscountFactor).toEqual('0.1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,22 +5,39 @@ import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
|
||||
import { useEstimateFeesQuery } from './__generated__/EstimateOrder';
|
||||
|
||||
const divideByTwo = (n: string) => (BigInt(n) / BigInt(2)).toString();
|
||||
export const sumFeesDiscounts = (
|
||||
fees: EstimateFeesQuery['estimateFees']['fees']
|
||||
) => {
|
||||
const volume = (
|
||||
BigInt(fees.makerFeeVolumeDiscount || '0') +
|
||||
BigInt(fees.infrastructureFeeVolumeDiscount || '0') +
|
||||
BigInt(fees.liquidityFeeVolumeDiscount || '0')
|
||||
).toString();
|
||||
const referral = (
|
||||
BigInt(fees.makerFeeReferralDiscount || '0') +
|
||||
BigInt(fees.infrastructureFeeReferralDiscount || '0') +
|
||||
BigInt(fees.liquidityFeeReferralDiscount || '0')
|
||||
).toString();
|
||||
return {
|
||||
volume,
|
||||
referral,
|
||||
total: (BigInt(volume) + BigInt(referral)).toString(),
|
||||
};
|
||||
};
|
||||
|
||||
export const sumFees = (fees: EstimateFeesQuery['estimateFees']['fees']) =>
|
||||
(
|
||||
BigInt(fees.makerFee || '0') +
|
||||
BigInt(fees.infrastructureFee || '0') +
|
||||
BigInt(fees.liquidityFee || '0')
|
||||
).toString();
|
||||
|
||||
export const useEstimateFees = (
|
||||
order?: OrderSubmissionBody['orderSubmission'],
|
||||
isMarketInAuction?: boolean
|
||||
):
|
||||
| (EstimateFeesQuery['estimateFees'] & {
|
||||
referralDiscountFactor: string;
|
||||
volumeDiscountFactor: string;
|
||||
})
|
||||
| undefined => {
|
||||
): EstimateFeesQuery['estimateFees'] | undefined => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const {
|
||||
data: currentData,
|
||||
previousData,
|
||||
loading,
|
||||
} = useEstimateFeesQuery({
|
||||
const { data } = useEstimateFeesQuery({
|
||||
variables: order && {
|
||||
marketId: order.marketId,
|
||||
partyId: pubKey || '',
|
||||
@@ -33,20 +50,8 @@ export const useEstimateFees = (
|
||||
fetchPolicy: 'no-cache',
|
||||
skip: !pubKey || !order?.size || !order?.price || order.postOnly,
|
||||
});
|
||||
const data = loading ? currentData || previousData : currentData;
|
||||
const atEpoch = (Number(data?.epoch.id) || 0) - 1;
|
||||
const volumeDiscountFactor =
|
||||
(data?.volumeDiscountStats.edges[0]?.node.atEpoch === atEpoch &&
|
||||
data?.volumeDiscountStats.edges[0]?.node.discountFactor) ||
|
||||
'0';
|
||||
const referralDiscountFactor =
|
||||
(data?.referralSetStats.edges[0]?.node.atEpoch === atEpoch &&
|
||||
data?.referralSetStats.edges[0]?.node.discountFactor) ||
|
||||
'0';
|
||||
if (order?.postOnly) {
|
||||
return {
|
||||
volumeDiscountFactor,
|
||||
referralDiscountFactor,
|
||||
totalFeeAmount: '0',
|
||||
fees: {
|
||||
infrastructureFee: '0',
|
||||
@@ -55,13 +60,8 @@ export const useEstimateFees = (
|
||||
},
|
||||
};
|
||||
}
|
||||
if (!data?.estimateFees) {
|
||||
return undefined;
|
||||
}
|
||||
return isMarketInAuction
|
||||
return isMarketInAuction && data?.estimateFees
|
||||
? {
|
||||
volumeDiscountFactor,
|
||||
referralDiscountFactor,
|
||||
totalFeeAmount: divideByTwo(data.estimateFees.totalFeeAmount),
|
||||
fees: {
|
||||
infrastructureFee: divideByTwo(
|
||||
@@ -91,9 +91,5 @@ export const useEstimateFees = (
|
||||
divideByTwo(data.estimateFees.fees.makerFeeVolumeDiscount),
|
||||
},
|
||||
}
|
||||
: {
|
||||
volumeDiscountFactor,
|
||||
referralDiscountFactor,
|
||||
...data.estimateFees,
|
||||
};
|
||||
: data?.estimateFees;
|
||||
};
|
||||
|
||||
@@ -84,7 +84,6 @@ export const DocsLinks = VEGA_DOCS_URL
|
||||
ETH_DATA_SOURCES: `${VEGA_DOCS_URL}/concepts/trading-on-vega/data-sources#ethereum-data-sources`,
|
||||
ICEBERG_ORDERS: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#iceberg-order`,
|
||||
POST_REDUCE_ONLY: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#conditional-order-parameters`,
|
||||
QUANTUM: `${VEGA_DOCS_URL}/concepts/assets/asset-framework#quantum`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import { useCallback, useRef, useState } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { FillsTable } from './fills-table';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { Pagination } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import { fillsWithMarketProvider } from './fills-data-provider';
|
||||
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface FillsManagerProps {
|
||||
partyId: string;
|
||||
@@ -52,13 +52,31 @@ export const FillsManager = ({
|
||||
overlayNoRowsTemplate={error ? error.message : t('No fills')}
|
||||
{...props}
|
||||
/>
|
||||
<Pagination
|
||||
count={data?.length || 0}
|
||||
pageInfo={pageInfo}
|
||||
showRetentionMessage={true}
|
||||
onLoad={load}
|
||||
hasDisplayedRows={hasDisplayedRow || false}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between border-t border-default p-1 items-center">
|
||||
<div className="text-xs">
|
||||
{t(
|
||||
'Depending on data node retention you may not be able see the "full" history'
|
||||
)}
|
||||
</div>
|
||||
<div className="flex text-xs items-center">
|
||||
{data?.length && !pageInfo?.hasNextPage
|
||||
? t('all %s items loaded', [data.length.toString()])
|
||||
: t('%s items loaded', [
|
||||
data?.length ? data.length.toString() : ' ',
|
||||
])}
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button size="extra-small" className="ml-1" onClick={() => load()}>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{data?.length && hasDisplayedRow === false ? (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
|
||||
{t('No fills matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ export const fundingPaymentsProvider = makeDataProvider<
|
||||
pagination: {
|
||||
getPageInfo,
|
||||
append,
|
||||
first: 1000,
|
||||
first: 100,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { FundingPaymentsTable } from './funding-payments-table';
|
||||
import { Pagination } from '@vegaprotocol/datagrid';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { fundingPaymentsWithMarketProvider } from './funding-payments-data-provider';
|
||||
@@ -21,17 +20,7 @@ export const FundingPaymentsManager = ({
|
||||
gridProps,
|
||||
}: FundingPaymentsManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
const { onFilterChanged, ...props } = gridProps || {};
|
||||
const onRowDataUpdated = useCallback(
|
||||
({ api }: { api: AgGridReact['api'] }) => {
|
||||
setHasDisplayedRow(!!api.getDisplayedRowCount());
|
||||
},
|
||||
[]
|
||||
);
|
||||
const { data, error, load, pageInfo } = useDataProvider({
|
||||
const { data, error } = useDataProvider({
|
||||
dataProvider: fundingPaymentsWithMarketProvider,
|
||||
update: ({ data }) => {
|
||||
if (data?.length && gridRef.current?.api) {
|
||||
@@ -44,26 +33,12 @@ export const FundingPaymentsManager = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<FundingPaymentsTable
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
onMarketClick={onMarketClick}
|
||||
onFilterChanged={(event) => {
|
||||
onRowDataUpdated(event);
|
||||
onFilterChanged(event);
|
||||
}}
|
||||
onRowDataUpdated={onRowDataUpdated}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No funding payments')}
|
||||
{...props}
|
||||
/>
|
||||
<Pagination
|
||||
count={data?.length || 0}
|
||||
pageInfo={pageInfo}
|
||||
onLoad={load}
|
||||
hasDisplayedRows={hasDisplayedRow || false}
|
||||
showRetentionMessage={true}
|
||||
/>
|
||||
</div>
|
||||
<FundingPaymentsTable
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
onMarketClick={onMarketClick}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No funding payments')}
|
||||
{...gridProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,19 +20,6 @@ fragment LiquidityProvisionFields on LiquidityProvision {
|
||||
status
|
||||
}
|
||||
|
||||
query PaidFees($marketId: ID) {
|
||||
paidLiquidityFees(marketId: $marketId) {
|
||||
edges {
|
||||
node {
|
||||
marketId
|
||||
assetId
|
||||
epoch
|
||||
totalFeesPaid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query LiquidityProvisions($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
liquiditySLAParameters {
|
||||
|
||||
@@ -5,13 +5,6 @@ import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type LiquidityProvisionFieldsFragment = { __typename?: 'LiquidityProvision', id: string, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } };
|
||||
|
||||
export type PaidFeesQueryVariables = Types.Exact<{
|
||||
marketId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type PaidFeesQuery = { __typename?: 'Query', paidLiquidityFees?: { __typename?: 'PaidLiquidityFeesConnection', edges: Array<{ __typename?: 'PaidLiquidityFeesEdge', node: { __typename?: 'PaidLiquidityFees', marketId: string, assetId: string, epoch: number, totalFeesPaid: string } } | null> } | null };
|
||||
|
||||
export type LiquidityProvisionsQueryVariables = Types.Exact<{
|
||||
marketId: Types.Scalars['ID'];
|
||||
}>;
|
||||
@@ -86,48 +79,6 @@ export const LiquidityProviderFieldsFragmentDoc = gql`
|
||||
}
|
||||
${LiquidityProviderFeeShareFieldsFragmentDoc}
|
||||
${LiquidityProviderSLAFieldsFragmentDoc}`;
|
||||
export const PaidFeesDocument = gql`
|
||||
query PaidFees($marketId: ID) {
|
||||
paidLiquidityFees(marketId: $marketId) {
|
||||
edges {
|
||||
node {
|
||||
marketId
|
||||
assetId
|
||||
epoch
|
||||
totalFeesPaid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __usePaidFeesQuery__
|
||||
*
|
||||
* To run a query within a React component, call `usePaidFeesQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `usePaidFeesQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = usePaidFeesQuery({
|
||||
* variables: {
|
||||
* marketId: // value for 'marketId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function usePaidFeesQuery(baseOptions?: Apollo.QueryHookOptions<PaidFeesQuery, PaidFeesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<PaidFeesQuery, PaidFeesQueryVariables>(PaidFeesDocument, options);
|
||||
}
|
||||
export function usePaidFeesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<PaidFeesQuery, PaidFeesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<PaidFeesQuery, PaidFeesQueryVariables>(PaidFeesDocument, options);
|
||||
}
|
||||
export type PaidFeesQueryHookResult = ReturnType<typeof usePaidFeesQuery>;
|
||||
export type PaidFeesLazyQueryHookResult = ReturnType<typeof usePaidFeesLazyQuery>;
|
||||
export type PaidFeesQueryResult = Apollo.QueryResult<PaidFeesQuery, PaidFeesQueryVariables>;
|
||||
export const LiquidityProvisionsDocument = gql`
|
||||
query LiquidityProvisions($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { Market, MarketMaybeWithDataAndCandles } from './markets-provider';
|
||||
import {
|
||||
calcTradedFactor,
|
||||
filterAndSortMarkets,
|
||||
sumFeesFactors,
|
||||
totalFeesFactorsPercentage,
|
||||
} from './market-utils';
|
||||
const { MarketState, MarketTradingMode } = Schema;
|
||||
@@ -72,10 +71,10 @@ describe('totalFeesFactorsPercentage', () => {
|
||||
makerFee: f[2].toString(),
|
||||
});
|
||||
it.each([
|
||||
{ i: createFee(0, 0, 1), o: '100%' },
|
||||
{ i: createFee(0, 1, 0), o: '100%' },
|
||||
{ i: createFee(1, 0, 0), o: '100%' },
|
||||
{ i: createFee(0.01, 0.02, 0.003), o: '3.3%' },
|
||||
{ i: createFee(0, 0, 1), o: '100.00%' },
|
||||
{ i: createFee(0, 1, 0), o: '100.00%' },
|
||||
{ i: createFee(1, 0, 0), o: '100.00%' },
|
||||
{ i: createFee(0.01, 0.02, 0.003), o: '3.30%' },
|
||||
{ i: createFee(0.01, 0.056782, 0.003), o: '6.9782%' },
|
||||
{ i: createFee(0.01, 0.056782, 0), o: '6.6782%' },
|
||||
])('adds fees correctly', ({ i, o }) => {
|
||||
@@ -133,15 +132,3 @@ describe('calcTradedFactor', () => {
|
||||
expect(fa > fb).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sumFeesFactors', () => {
|
||||
it('does not result in flop errors', () => {
|
||||
expect(
|
||||
sumFeesFactors({
|
||||
makerFee: '0.1',
|
||||
infrastructureFee: '0.2',
|
||||
liquidityFee: '0.3',
|
||||
})
|
||||
).toEqual(0.6);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,19 +50,16 @@ export const getQuoteName = (market: Partial<Market>) => {
|
||||
};
|
||||
|
||||
export const sumFeesFactors = (fees: Market['fees']['factors']) => {
|
||||
if (!fees) return;
|
||||
|
||||
return new BigNumber(fees.makerFee)
|
||||
.plus(fees.liquidityFee)
|
||||
.plus(fees.infrastructureFee)
|
||||
.toNumber();
|
||||
return fees
|
||||
? new BigNumber(fees.makerFee)
|
||||
.plus(fees.liquidityFee)
|
||||
.plus(fees.infrastructureFee)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
export const totalFeesFactorsPercentage = (fees: Market['fees']['factors']) => {
|
||||
const total = fees && sumFeesFactors(fees);
|
||||
return total
|
||||
? formatNumberPercentage(new BigNumber(total).times(100))
|
||||
: undefined;
|
||||
return total ? formatNumberPercentage(total.times(100)) : undefined;
|
||||
};
|
||||
|
||||
export const filterAndSortMarkets = (markets: MarketMaybeWithData[]) => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { OrderListTable } from '../order-list';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { Pagination } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
|
||||
import { normalizeOrderAmendment } from '@vegaprotocol/wallet';
|
||||
@@ -12,7 +11,7 @@ import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/web3';
|
||||
import { OrderEditDialog } from '../order-list/order-edit-dialog';
|
||||
import type { Order } from '../order-data-provider';
|
||||
import { OrderViewDialog } from '../order-list/order-view-dialog';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { Splash, TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export enum Filter {
|
||||
'Open' = 'Open',
|
||||
@@ -91,34 +90,61 @@ export const OrderListManager = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex flex-col h-full">
|
||||
<OrderListTable
|
||||
rowData={data}
|
||||
ref={gridRef}
|
||||
filter={filter}
|
||||
onCancel={cancel}
|
||||
onEdit={setEditOrder}
|
||||
onView={setViewOrder}
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
onFilterChanged={(event) => {
|
||||
onRowDataUpdated(event);
|
||||
if (onFilterChanged) {
|
||||
onFilterChanged(event);
|
||||
}
|
||||
}}
|
||||
onRowDataUpdated={onRowDataUpdated}
|
||||
isReadOnly={isReadOnly}
|
||||
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
|
||||
{...props}
|
||||
/>
|
||||
<Pagination
|
||||
count={data?.length || 0}
|
||||
pageInfo={pageInfo}
|
||||
onLoad={load}
|
||||
hasDisplayedRows={hasDisplayedRow || false}
|
||||
showRetentionMessage={variables.filter?.liveOnly || true}
|
||||
/>
|
||||
<div className="h-full relative">
|
||||
<div className="flex flex-col h-full">
|
||||
<OrderListTable
|
||||
rowData={data}
|
||||
ref={gridRef}
|
||||
filter={filter}
|
||||
onCancel={cancel}
|
||||
onEdit={setEditOrder}
|
||||
onView={setViewOrder}
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
onFilterChanged={(event) => {
|
||||
onRowDataUpdated(event);
|
||||
if (onFilterChanged) {
|
||||
onFilterChanged(event);
|
||||
}
|
||||
}}
|
||||
onRowDataUpdated={onRowDataUpdated}
|
||||
isReadOnly={isReadOnly}
|
||||
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
|
||||
{...props}
|
||||
/>
|
||||
<div className="flex justify-between border-t border-default p-1 items-center">
|
||||
<div className="text-xs">
|
||||
{variables.filter?.liveOnly
|
||||
? null
|
||||
: t(
|
||||
'Depending on data node retention you may not be able see the "full" history'
|
||||
)}
|
||||
</div>
|
||||
{data ? (
|
||||
<div className="flex text-xs items-center">
|
||||
{data?.length && !pageInfo?.hasNextPage
|
||||
? t('all %s items loaded', [data.length.toString()])
|
||||
: t('%s items loaded', [
|
||||
data?.length ? data.length.toString() : ' ',
|
||||
])}
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button
|
||||
size="extra-small"
|
||||
className="ml-1"
|
||||
onClick={() => load()}
|
||||
>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{data?.length && hasDisplayedRow === false ? (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
|
||||
{t('No orders matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{editOrder && (
|
||||
<OrderEditDialog
|
||||
|
||||
@@ -3,10 +3,10 @@ import { tradesWithMarketProvider } from './trades-data-provider';
|
||||
import { TradesTable } from './trades-table';
|
||||
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Pagination } from '@vegaprotocol/datagrid';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface TradesContainerProps {
|
||||
marketId: string;
|
||||
@@ -49,13 +49,30 @@ export const TradesManager = ({
|
||||
overlayNoRowsTemplate={error ? error.message : t('No trades')}
|
||||
{...props}
|
||||
/>
|
||||
<Pagination
|
||||
count={data?.length || 0}
|
||||
pageInfo={pageInfo}
|
||||
onLoad={load}
|
||||
hasDisplayedRows={hasDisplayedRow || false}
|
||||
showRetentionMessage={true}
|
||||
/>
|
||||
<div className="flex justify-between border-t border-default p-1 items-center">
|
||||
<div className="text-xs">
|
||||
{t(
|
||||
'Depending on data node retention you may not be able see the "full" history'
|
||||
)}
|
||||
</div>
|
||||
<div className="flex text-xs items-center">
|
||||
{data?.length && !pageInfo?.hasNextPage
|
||||
? t('all %s items loaded', [data.length.toString()])
|
||||
: t('%s items loaded', [
|
||||
data?.length ? data.length.toString() : ' ',
|
||||
])}
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button size="extra-small" className="ml-1" onClick={() => load()}>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{data?.length && hasDisplayedRow === false ? (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
|
||||
{t('No trades matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Generated
+2
-6
@@ -813,7 +813,7 @@ export type DispatchStrategy = {
|
||||
/** Minimum notional time-weighted averaged position required for a party to be considered eligible */
|
||||
notionalTimeWeightedAveragePositionRequirement: Scalars['String'];
|
||||
/** Ascending order list of start rank and corresponding share ratio */
|
||||
rankTable?: Maybe<Array<Maybe<RankTable>>>;
|
||||
rankTable?: Maybe<RankTable>;
|
||||
/** Minimum number of governance tokens, e.g. VEGA, staked for a party to be considered eligible */
|
||||
stakingRequirement: Scalars['String'];
|
||||
/** The teams in scope for the reward, if the entity is teams */
|
||||
@@ -3624,8 +3624,6 @@ export type PartyVestingStats = {
|
||||
__typename?: 'PartyVestingStats';
|
||||
/** Epoch for which the statistics are valid */
|
||||
epochSeq: Scalars['Int'];
|
||||
/** The balance of the party, in quantum. */
|
||||
quantumBalance: Scalars['String'];
|
||||
/** The reward bonus multiplier */
|
||||
rewardBonusMultiplier: Scalars['String'];
|
||||
};
|
||||
@@ -4833,7 +4831,7 @@ export type QueryprotocolUpgradeProposalsArgs = {
|
||||
|
||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||
export type QueryreferralSetRefereesArgs = {
|
||||
aggregationEpochs?: InputMaybe<Scalars['Int']>;
|
||||
aggregationDays?: InputMaybe<Scalars['Int']>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
pagination?: InputMaybe<Pagination>;
|
||||
referee?: InputMaybe<Scalars['ID']>;
|
||||
@@ -5090,8 +5088,6 @@ export type ReferralSetStats = {
|
||||
partyId: Scalars['ID'];
|
||||
/** Running volume for the set based on the window length of the current referral program. */
|
||||
referralSetRunningNotionalTakerVolume: Scalars['String'];
|
||||
/** The referrer's taker volume */
|
||||
referrerTakerVolume: Scalars['String'];
|
||||
/** Reward factor applied to the party. */
|
||||
rewardFactor: Scalars['String'];
|
||||
/** The proportion of the referees taker fees to be rewarded to the referrer. */
|
||||
|
||||
@@ -72,7 +72,7 @@ LessThan24HoursIncrease.args = {
|
||||
|
||||
export const LessThan24HoursDecrease = Template.bind({});
|
||||
LessThan24HoursDecrease.args = {
|
||||
data: [20990000, 20939973, 20980130],
|
||||
data: [20, 21, 22, 23, 24, 6, 7, 9, 11, 13, 11, 9],
|
||||
width: 110,
|
||||
height: 30,
|
||||
};
|
||||
|
||||
@@ -44,12 +44,14 @@ export const SparklineView = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const midValue = (min + max) / 2;
|
||||
|
||||
// Market may be less than 24hr old so padd the data array
|
||||
// with values that is the mid value (avg of min and max).
|
||||
// This will rendera horizontal line until the real data shifts the line
|
||||
const trimmedData = data.slice(-points);
|
||||
const padCount = data.length < points ? points - data.length : 0;
|
||||
const padArr = new Array(padCount).fill(trimmedData[0]);
|
||||
const padArr = new Array(padCount).fill(midValue);
|
||||
const trimmedData = data.slice(-points);
|
||||
|
||||
// Get the last 24 values if data has more than needed
|
||||
const lineData: [number, number][] = [...padArr, ...trimmedData].map(
|
||||
|
||||
@@ -77,8 +77,8 @@ describe('number utils', () => {
|
||||
{ v: new BigNumber(123.123), d: 3, o: '123.123%' },
|
||||
{ v: new BigNumber(123.123), d: 6, o: '123.123%' },
|
||||
{ v: new BigNumber(123.123), d: 0, o: '123%' },
|
||||
{ v: new BigNumber(123), d: undefined, o: '123%' }, // it default to 2 decimal places
|
||||
{ v: new BigNumber(30000), d: undefined, o: '30,000%' },
|
||||
{ v: new BigNumber(123), d: undefined, o: '123.00%' }, // it default to 2 decimal places
|
||||
{ v: new BigNumber(30000), d: undefined, o: '30,000.00%' },
|
||||
{ v: new BigNumber(3.000001), d: undefined, o: '3.000001%' },
|
||||
])('formats given number correctly', ({ v, d, o }) => {
|
||||
expect(formatNumberPercentage(v, d)).toStrictEqual(o);
|
||||
|
||||
@@ -158,7 +158,7 @@ export const addDecimalsFixedFormatNumber = (
|
||||
|
||||
export const formatNumberPercentage = (value: BigNumber, decimals?: number) => {
|
||||
const decimalPlaces =
|
||||
typeof decimals === 'undefined' ? value.dp() || 0 : decimals;
|
||||
typeof decimals === 'undefined' ? Math.max(value.dp() || 0, 2) : decimals;
|
||||
return `${formatNumber(value, decimalPlaces)}%`;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||
import type { Market, Order } from '@vegaprotocol/types';
|
||||
import type { AccountType } from '@vegaprotocol/types';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { ethers } from 'ethers';
|
||||
import { sha3_256 } from 'js-sha3';
|
||||
@@ -47,17 +47,15 @@ export const normalizeOrderAmendment = <T extends Exact<OrderAmendment, T>>(
|
||||
export const normalizeTransfer = <T extends Exact<Transfer, T>>(
|
||||
address: string,
|
||||
amount: string,
|
||||
fromAccountType: AccountType,
|
||||
toAccountType: AccountType,
|
||||
asset: {
|
||||
id: string;
|
||||
decimals: number;
|
||||
}
|
||||
): Transfer => {
|
||||
return {
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: address,
|
||||
fromAccountType,
|
||||
toAccountType,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
asset: asset.id,
|
||||
amount: removeDecimal(amount, asset.decimals),
|
||||
// oneOff or recurring required otherwise wallet will error
|
||||
|
||||
+2
-4
@@ -1,4 +1,2 @@
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
[functions]
|
||||
included_files = ["!node_modules/@sentry/cli/sentry-cli"]
|
||||
|
||||
Reference in New Issue
Block a user