Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8461a0a0d5 | ||
|
|
80514a4adc | ||
|
|
e4f0cbc822 | ||
|
|
a9fd184647 | ||
|
|
e06ec8bb24 |
@@ -39,6 +39,7 @@ context('Market page', { tags: '@regression' }, function () {
|
||||
cy.contains('Test market 1').click();
|
||||
cy.getByTestId(marketHeaders).should('have.text', 'Test market 1');
|
||||
cy.validate_element_from_table('Name', 'Test market 1');
|
||||
cy.validate_element_from_table('Market ID', this.createdMarketId);
|
||||
cy.validate_element_from_table('Trading Mode', 'Opening auction');
|
||||
cy.validate_element_from_table('Market Decimal Places', '5');
|
||||
cy.validate_element_from_table('Position Decimal Places', '5');
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
SPECIAL_CASE_NETWORK,
|
||||
SPECIAL_CASE_NETWORK_ID,
|
||||
} from '../../links/party-link/party-link';
|
||||
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
|
||||
|
||||
type Transfer = components['schemas']['commandsv1Transfer'];
|
||||
|
||||
@@ -64,16 +63,10 @@ export const TxDetailsTransfer = ({
|
||||
return (
|
||||
<>
|
||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||
<TableRow modifier="bordered" data-testid="type">
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
|
||||
<TableCell>{getTypeLabelForTransfer(transfer)}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered" data-testid="id">
|
||||
<TableCell {...sharedHeaderProps}>{t('Transfer ID')}</TableCell>
|
||||
<TableCell>
|
||||
{txSignatureToDeterministicId(txData.signature.value)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TxDetailsShared
|
||||
txData={txData}
|
||||
pubKey={pubKey}
|
||||
@@ -81,7 +74,7 @@ export const TxDetailsTransfer = ({
|
||||
hideTypeRow={true}
|
||||
/>
|
||||
{from ? (
|
||||
<TableRow modifier="bordered" data-testid="from">
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('From')}</TableCell>
|
||||
<TableCell>
|
||||
<PartyLink id={from} />
|
||||
@@ -89,7 +82,7 @@ export const TxDetailsTransfer = ({
|
||||
</TableRow>
|
||||
) : null}
|
||||
{transfer.to ? (
|
||||
<TableRow modifier="bordered" data-testid="to">
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('To')}</TableCell>
|
||||
<TableCell>
|
||||
<PartyLink id={transfer.to} />
|
||||
@@ -97,7 +90,7 @@ export const TxDetailsTransfer = ({
|
||||
</TableRow>
|
||||
) : null}
|
||||
{transfer.asset && transfer.amount ? (
|
||||
<TableRow modifier="bordered" data-testid="amount">
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Amount')}</TableCell>
|
||||
<TableCell>
|
||||
<SizeInAsset assetId={transfer.asset} size={transfer.amount} />
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
|
||||
import type { TendermintBlocksResponse } from '../../routes/blocks/tendermint-blocks-response';
|
||||
import { getTypeLabelForTransfer } from './details/tx-transfer';
|
||||
import type { components } from '../../../types/explorer';
|
||||
|
||||
import {
|
||||
TxDetailsTransfer,
|
||||
getTypeLabelForTransfer,
|
||||
} from './details/tx-transfer';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
type Transfer = components['schemas']['commandsv1Transfer'];
|
||||
|
||||
describe('TX: Transfer: getLabelForTransfer', () => {
|
||||
@@ -65,70 +56,3 @@ describe('TX: Transfer: getLabelForTransfer', () => {
|
||||
expect(getTypeLabelForTransfer(mock)).toEqual('Transfer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TxDetailsTransfer', () => {
|
||||
const mockBlockData = {
|
||||
result: {
|
||||
block: {
|
||||
header: {
|
||||
height: '123',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockTxData: Partial<BlockExplorerTransactionResult> = {
|
||||
hash: 'test',
|
||||
submitter:
|
||||
'e1943eea46fed576cf2be42972f3c5515ad3d0ac7ac013f56677c12a53a1b3ed',
|
||||
command: {
|
||||
nonce: '5188810881378065222',
|
||||
blockHeight: '14951513',
|
||||
transfer: {
|
||||
fromAccountType: 'ACCOUNT_TYPE_GENERAL',
|
||||
to: '78432a2808f20b18a46ccc6a917bdc4d63c2b9e7007f777bdcab5a9f462c5ba6',
|
||||
toAccountType: 'ACCOUNT_TYPE_GENERAL',
|
||||
asset:
|
||||
'dd20590509d30d20bdbbe64dc1090c1140c7690121a9b9940bc66f62dfa2e599',
|
||||
amount: '4800000000',
|
||||
reference: '',
|
||||
oneOff: {
|
||||
deliverOn: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
signature: {
|
||||
value:
|
||||
'610c2e196a7d4fed4413b9e82af267b1ff3e30e943df3a3d28096fd60604d430d752fbaf6dd4f84d496be78885bb6118f40560bff7832c06bd7a3d67b718b700',
|
||||
},
|
||||
};
|
||||
|
||||
it('renders basic transfer details', () => {
|
||||
const { getByTestId } = render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
<TxDetailsTransfer
|
||||
txData={mockTxData as BlockExplorerTransactionResult}
|
||||
pubKey={mockTxData.command.submitter}
|
||||
blockData={mockBlockData as TendermintBlocksResponse}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
const id = getByTestId('id');
|
||||
expect(id.children[0].textContent).toEqual('Transfer ID');
|
||||
expect(id.children[1].textContent).toEqual(
|
||||
'51f3bab5eb2637651012507a64d497790a734248792c16e5cf36df8984074fbd'
|
||||
);
|
||||
|
||||
const type = getByTestId('type');
|
||||
expect(type.children[1].textContent).toEqual('Transfer');
|
||||
|
||||
const from = getByTestId('from');
|
||||
expect(from.children[1].textContent).toEqual(mockTxData.submitter);
|
||||
|
||||
const to = getByTestId('to');
|
||||
expect(to.children[1].textContent).toEqual(mockTxData.command.transfer.to);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -537,10 +537,10 @@ describe(
|
||||
cy.VegaWalletSubmitProposal(createGovernanceTransferProposalTxBody());
|
||||
cy.reload();
|
||||
getProposalFromTitle('Governance transfer proposal').within(() => {
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
|
||||
cy.getByTestId(governanceTransferToggle).click();
|
||||
cy.getByTestId('proposal-transfer-details-table').within(() => {
|
||||
getProposalInformationFromTable('Source Type')
|
||||
@@ -590,7 +590,7 @@ describe(
|
||||
);
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'Cancel transfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'CancelTransfer');
|
||||
getProposalInformationFromTable('Error details')
|
||||
.invoke('text')
|
||||
.and('eq', 'Governance transfer invalid transfer id not found');
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -128,7 +128,9 @@ describe('<ProposalReferralProgramDetails />', () => {
|
||||
it('should not render if there are no relevant fields', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {},
|
||||
change: {
|
||||
__typename: 'UpdateReferralProgram',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './proposal-update-benefit-tiers-details';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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">
|
||||
|
||||
@@ -7,7 +7,7 @@ export type NewMarketProductFieldsFragment = { __typename?: 'Proposal', terms: {
|
||||
|
||||
export type UpdateMarketStatesFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
|
||||
|
||||
export type UpdateReferralProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: any, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
|
||||
export type UpdateReferralProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: string, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
|
||||
|
||||
export type UpdateVolumeDiscountProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } } };
|
||||
|
||||
@@ -21,7 +21,7 @@ export type ProposalsQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null, product?: { __typename: 'FutureProduct' } | { __typename: 'PerpetualProduct' } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: any, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null };
|
||||
export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null, product?: { __typename: 'FutureProduct' } | { __typename: 'PerpetualProduct' } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: string, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null };
|
||||
|
||||
export const NewMarketProductFieldsFragmentDoc = gql`
|
||||
fragment NewMarketProductFields on Proposal {
|
||||
|
||||
@@ -142,12 +142,6 @@ export const generateYesVotes = (
|
||||
})
|
||||
.toString(),
|
||||
},
|
||||
vestingBalancesSummary: {
|
||||
__typename: 'PartyVestingBalancesSummary',
|
||||
epoch: null,
|
||||
lockedBalances: [],
|
||||
vestingBalances: [],
|
||||
},
|
||||
},
|
||||
datetime: faker.date.past().toISOString(),
|
||||
};
|
||||
@@ -198,12 +192,6 @@ export const generateNoVotes = (
|
||||
})
|
||||
.toString(),
|
||||
},
|
||||
vestingBalancesSummary: {
|
||||
__typename: 'PartyVestingBalancesSummary',
|
||||
epoch: null,
|
||||
lockedBalances: [],
|
||||
vestingBalances: [],
|
||||
},
|
||||
},
|
||||
datetime: faker.date.past().toISOString(),
|
||||
};
|
||||
|
||||
@@ -48,8 +48,8 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||
cy.get('@markets').then((markets) => {
|
||||
cy.wrap(markets[0]).as('market');
|
||||
});
|
||||
cy.setOnBoardingViewed();
|
||||
cy.visit('/#/portfolio');
|
||||
cy.connectVegaWallet();
|
||||
});
|
||||
|
||||
it('can deposit', function () {
|
||||
@@ -70,8 +70,6 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
selectAsset(btcName);
|
||||
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
|
||||
|
||||
cy.getByTestId('approve-default').should(
|
||||
'contain.text',
|
||||
`Before you can make a deposit of your chosen asset, ${btcSymbol}, you need to approve its use in your Ethereum wallet`
|
||||
@@ -122,7 +120,7 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
|
||||
|
||||
cy.getByTestId(collateralTab).click();
|
||||
cy.getByTestId('open-transfer').eq(1).click();
|
||||
cy.getByTestId('open-transfer').click();
|
||||
cy.getByTestId('transfer-form').should('be.visible');
|
||||
cy.getByTestId('transfer-form').find('[name="toAddress"]').select(1);
|
||||
cy.get('select option')
|
||||
@@ -149,8 +147,7 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||
// 0003-WTXN-011
|
||||
cy.getByTestId('Withdrawals').click();
|
||||
cy.getByTestId('withdraw-dialog-button').click();
|
||||
selectAsset(btcName);
|
||||
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
|
||||
selectAsset(0);
|
||||
cy.get(amountField).focus();
|
||||
cy.get(amountField).clear().type('1');
|
||||
cy.getByTestId('submit-withdrawal').click();
|
||||
@@ -183,21 +180,24 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
|
||||
cy.get('@markets').then((markets) => {
|
||||
cy.wrap(markets[0]).as('market');
|
||||
});
|
||||
cy.setOnBoardingViewed();
|
||||
cy.setVegaWallet();
|
||||
});
|
||||
|
||||
it('shows node health', function () {
|
||||
// 0006-NETW-010
|
||||
const regex = /^Operational\d+$/;
|
||||
const market = this.market;
|
||||
cy.visit(`/#/markets/${market.id}`);
|
||||
cy.getByTestId('node-health-trigger').realHover();
|
||||
cy.getByTestId('node-health')
|
||||
.children()
|
||||
.first()
|
||||
.invoke('text')
|
||||
.should('match', regex);
|
||||
.should('contain.text', 'Operational')
|
||||
.then(($el) => {
|
||||
const blockHeight = parseInt($el.text());
|
||||
// block height will increase over the course of the test run so best
|
||||
// we can do here is check that its showing something sensible
|
||||
expect(blockHeight).to.be.greaterThan(0);
|
||||
});
|
||||
cy.getByTestId('node-health')
|
||||
.children()
|
||||
.eq(1)
|
||||
@@ -239,6 +239,7 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
|
||||
.should('contain.text', order.size);
|
||||
|
||||
cy.getByTestId(openOrdersTab).click();
|
||||
cy.getByTestId('edit', txTimeout).should('contain.text', 'Edit');
|
||||
cy.getByTestId('tab-open-orders').within(() => {
|
||||
cy.get('.ag-center-cols-container')
|
||||
.children()
|
||||
@@ -279,7 +280,8 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
|
||||
cy.visit(`/#/markets/${market.id}`);
|
||||
cy.getByTestId(toastCloseBtn, txTimeout).click();
|
||||
cy.getByTestId(openOrdersTab).click();
|
||||
cy.getByTestId('edit').first().click();
|
||||
cy.getByTestId('edit', txTimeout).should('be.visible');
|
||||
cy.getByTestId('edit').first().should('be.visible').click();
|
||||
cy.getByTestId('dialog-title').should('contain.text', 'Edit order');
|
||||
cy.get('#limitPrice').focus().clear().type(newPrice);
|
||||
cy.getByTestId('edit-order').find('[type="submit"]').click();
|
||||
@@ -348,7 +350,6 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
|
||||
cy.getByTestId('withdraw-dialog-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
selectAsset(btcName);
|
||||
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
|
||||
cy.get(amountField).clear().type('1');
|
||||
cy.getByTestId('submit-withdrawal').click();
|
||||
cy.getByTestId(toastContent, txTimeout).should(
|
||||
@@ -436,7 +437,6 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
selectAsset(btcName);
|
||||
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
|
||||
cy.contains('Deposits of tBTC not approved').should('not.exist');
|
||||
cy.contains('Use maximum').should('be.visible');
|
||||
cy.get(amountField).clear().type('20000000');
|
||||
|
||||
@@ -69,7 +69,7 @@ describe(
|
||||
cy.contains('Something went wrong').should('not.exist');
|
||||
cy.contains('Application error').should('not.exist');
|
||||
cy.getByTestId('tab-liquidity').within(() => {
|
||||
cy.get('[col-id="partyId"]').eq(1).should('not.be.empty');
|
||||
cy.get('[col-id="party.id"]').eq(1).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ const marketSummaryBlock = 'header-summary';
|
||||
const itemValue = 'item-value';
|
||||
const itemHeader = 'item-header';
|
||||
const colCommitmentAmount = '[col-id="commitmentAmount"]';
|
||||
const colEquityLikeShare = '[col-id="feeShare.equityLikeShare"]';
|
||||
const colAverageEntryValuation = '[col-id="averageEntryValuation"]';
|
||||
const colEquityLikeShare = '[col-id="equityLikeShare"]';
|
||||
const colFee = '[col-id="fee"]';
|
||||
const colCommitmentAmount_1 = '[col-id="commitmentAmount_1"]';
|
||||
const colBalance = '[col-id="balance"]';
|
||||
@@ -23,17 +24,11 @@ const colUpdatedAt = '[col-id="updatedAt"] button';
|
||||
const headers = [
|
||||
'Party',
|
||||
'Commitment (tDAI)',
|
||||
'Obligation',
|
||||
'Fee',
|
||||
'Adjusted stake share',
|
||||
'Share',
|
||||
'Live supplied liquidity',
|
||||
'Fees accrued this epoch',
|
||||
'Live time on book',
|
||||
'Live liquidity quality score (%)',
|
||||
'Last time on the book',
|
||||
'Last fee penalty',
|
||||
'Last bond penalty',
|
||||
'Proposed fee',
|
||||
'Market valuation at entry',
|
||||
'Obligation',
|
||||
'Supplied',
|
||||
'Status',
|
||||
'Created',
|
||||
'Updated',
|
||||
@@ -69,8 +64,11 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
|
||||
// 5002-LIQP-002
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="partyId"]')
|
||||
.should('have.text', '69464e…dc6f');
|
||||
.find('[col-id="party.id"]')
|
||||
.should(
|
||||
'have.text',
|
||||
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
|
||||
);
|
||||
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
@@ -84,6 +82,12 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
|
||||
|
||||
cy.get(rowSelector).first().find(colFee).should('have.text', '0.09%');
|
||||
|
||||
// 5002-LIQP-013
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find(colAverageEntryValuation)
|
||||
.should('have.text', '685,852.93692');
|
||||
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find(colCommitmentAmount_1)
|
||||
@@ -100,8 +104,7 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
|
||||
cy.get(rowSelector).first().find(colCreatedAt).should('not.be.empty');
|
||||
cy.get(rowSelector).first().find(colUpdatedAt).should('not.be.empty');
|
||||
});
|
||||
|
||||
it('liquidity status column should be sorted properly', () => {
|
||||
it.skip('liquidity status column should be sorted properly', () => {
|
||||
// 5002-LIQP-003
|
||||
const liquidityColDefault = ['Active', 'Pending'];
|
||||
const liquidityColAsc = ['Active', 'Pending'];
|
||||
@@ -217,8 +220,11 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
|
||||
// 5002-LIQP-011
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
.find('[col-id="partyId"]')
|
||||
.should('have.text', '69464e…dc6f');
|
||||
.find('[col-id="party.id"]')
|
||||
.should(
|
||||
'have.text',
|
||||
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
|
||||
);
|
||||
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
@@ -235,6 +241,12 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
|
||||
.find(colFee)
|
||||
.should('have.text', '0.09%');
|
||||
|
||||
// 5002-LIQP-013
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
.find(colAverageEntryValuation)
|
||||
.should('have.text', '685,852.93692');
|
||||
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
.find(colCommitmentAmount_1)
|
||||
@@ -265,8 +277,11 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('Inactive').click();
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
.find('[col-id="partyId"]')
|
||||
.should('have.text', 'cc464e…dc6f');
|
||||
.find('[col-id="party.id"]')
|
||||
.should(
|
||||
'have.text',
|
||||
'cc464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
|
||||
);
|
||||
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
@@ -283,6 +298,11 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
|
||||
.find(colFee)
|
||||
.should('have.text', '0.40%');
|
||||
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
.find(colAverageEntryValuation)
|
||||
.should('have.text', '685,852.93692');
|
||||
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
.find(colCommitmentAmount_1)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
TIFlist,
|
||||
orderPriceField,
|
||||
orderSizeField,
|
||||
orderTIFDropDown,
|
||||
placeOrderBtn,
|
||||
toggleLimit,
|
||||
toggleMarket,
|
||||
} from '../support/deal-ticket';
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import { accountsQuery } from '@vegaprotocol/mock';
|
||||
|
||||
describe('suspended market validation', { tags: '@regression' }, () => {
|
||||
before(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage(
|
||||
Schema.MarketState.STATE_SUSPENDED,
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
);
|
||||
const accounts = accountsQuery();
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'Accounts', accounts);
|
||||
});
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.setVegaWallet();
|
||||
});
|
||||
|
||||
it('should show warning for market order', function () {
|
||||
cy.getByTestId(toggleMarket).click();
|
||||
// 7002-SORD-060
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
cy.getByTestId('deal-ticket-error-message-type').should(
|
||||
'have.text',
|
||||
'This market is in auction until it reaches sufficient liquidity. Only limit orders are permitted when market is in auction'
|
||||
);
|
||||
});
|
||||
|
||||
it('should show info for allowed TIF', function () {
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
cy.getByTestId(orderPriceField).clear().type('0.1');
|
||||
cy.getByTestId(orderSizeField).clear().type('1');
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
cy.getByTestId('deal-ticket-warning-auction').should(
|
||||
'have.text',
|
||||
'Any orders placed now will not trade until the auction ends'
|
||||
);
|
||||
});
|
||||
|
||||
it('should show warning for not allowed TIF', function () {
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
cy.getByTestId(orderTIFDropDown).select(
|
||||
TIFlist.filter((item) => item.code === 'FOK')[0].value
|
||||
);
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
cy.getByTestId('deal-ticket-error-message-tif').should(
|
||||
'have.text',
|
||||
'This market is in auction until it reaches sufficient liquidity. Until the auction ends, you can only place GFA, GTT, or GTC limit orders'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import { fillsQuery } from '@vegaprotocol/mock';
|
||||
|
||||
const tabFills = 'tab-fills';
|
||||
|
||||
describe('fills', { tags: '@regression' }, () => {
|
||||
// 7005-FILL-001
|
||||
// 7005-FILL-002
|
||||
// 7005-FILL-003
|
||||
// 7005-FILL-004
|
||||
// 7005-FILL-005
|
||||
// 7005-FILL-006
|
||||
// 7005-FILL-007
|
||||
// 7005-FILL-008
|
||||
|
||||
beforeEach(() => {
|
||||
// Ensure page loads with correct key
|
||||
cy.window().then((window) => {
|
||||
cy.wrap(
|
||||
window.localStorage.setItem(
|
||||
'vega_wallet_key',
|
||||
Cypress.env('VEGA_PUBLIC_KEY')
|
||||
)
|
||||
);
|
||||
});
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
'Fills',
|
||||
fillsQuery({}, Cypress.env('VEGA_PUBLIC_KEY'))
|
||||
);
|
||||
});
|
||||
cy.mockSubscription();
|
||||
});
|
||||
|
||||
it('renders fills on portfolio page', () => {
|
||||
cy.visit('/#/portfolio');
|
||||
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
|
||||
cy.getByTestId('Fills').click();
|
||||
validateFillsDisplayed();
|
||||
});
|
||||
|
||||
it('renders fills on trading tab', () => {
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId('Fills').click();
|
||||
validateFillsDisplayed();
|
||||
});
|
||||
|
||||
function validateFillsDisplayed() {
|
||||
cy.getByTestId(tabFills).should('be.visible');
|
||||
cy.getByTestId(tabFills).contains('Market');
|
||||
cy.getByTestId(tabFills)
|
||||
.get(
|
||||
'[role="gridcell"][col-id="market.tradableInstrument.instrument.code"]'
|
||||
)
|
||||
.each(($marketSymbol) => {
|
||||
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
|
||||
});
|
||||
cy.getByTestId(tabFills).contains('Size');
|
||||
cy.get(`[col-id='size']`).eq(1).should('contain.text', '+');
|
||||
cy.get(`[col-id='size']`).eq(2).should('contain.text', '-');
|
||||
cy.getByTestId(tabFills)
|
||||
.get('[role="gridcell"][col-id="size"]')
|
||||
.each(($amount) => {
|
||||
cy.wrap($amount).invoke('text').should('not.be.empty');
|
||||
});
|
||||
cy.getByTestId(tabFills).contains('Price');
|
||||
cy.getByTestId(tabFills)
|
||||
.get('[role="gridcell"][col-id="price"]')
|
||||
.each(($prices) => {
|
||||
cy.wrap($prices).invoke('text').should('not.be.empty');
|
||||
});
|
||||
cy.getByTestId(tabFills).contains('Notional');
|
||||
cy.getByTestId(tabFills)
|
||||
.get('[role="gridcell"][col-id="price_1"]')
|
||||
.each(($total) => {
|
||||
cy.wrap($total).invoke('text').should('not.be.empty');
|
||||
});
|
||||
cy.getByTestId(tabFills).contains('Role');
|
||||
cy.getByTestId(tabFills)
|
||||
.get('[role="gridcell"][col-id="aggressor"]')
|
||||
.each(($role) => {
|
||||
cy.wrap($role)
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
const roles = ['Maker', 'Taker', '-'];
|
||||
expect(roles.indexOf(text.trim())).to.be.greaterThan(-1);
|
||||
});
|
||||
});
|
||||
cy.getByTestId(tabFills).contains('Fee');
|
||||
cy.getByTestId(tabFills)
|
||||
.get(
|
||||
'[role="gridcell"][col-id="market.tradableInstrument.instrument.product"]'
|
||||
)
|
||||
.each(($fees) => {
|
||||
cy.wrap($fees).invoke('text').should('not.be.empty');
|
||||
});
|
||||
cy.getByTestId(tabFills).contains('Date');
|
||||
const dateTimeRegex =
|
||||
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
|
||||
cy.get('[col-id="createdAt"]').each(($tradeDateTime, index) => {
|
||||
if (index != 0) {
|
||||
//ignore header
|
||||
cy.wrap($tradeDateTime).invoke('text').should('match', dateTimeRegex);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { selectAsset } from '../support/helpers';
|
||||
|
||||
const amountField = 'input[name="amount"]';
|
||||
const includeTransferFeeRadioBtn = 'include-transfer-fee';
|
||||
const manageVegaWallet = 'manage-vega-wallet';
|
||||
const toAddressField = '[name="toAddress"]';
|
||||
const totalTransferfee = 'total-transfer-fee';
|
||||
const transferAmount = 'transfer-amount';
|
||||
const transferForm = 'transfer-form';
|
||||
const transferFee = 'transfer-fee';
|
||||
const walletTransfer = 'wallet-transfer';
|
||||
|
||||
const ASSET_SEPOLIA_TBTC = 2;
|
||||
|
||||
describe.skip(
|
||||
'transfer fees',
|
||||
{ tags: '@regression', testIsolation: true },
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
cy.mockWeb3Provider();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.setVegaWallet();
|
||||
|
||||
cy.visit('/');
|
||||
cy.getByTestId(manageVegaWallet).click();
|
||||
cy.getByTestId(walletTransfer).click();
|
||||
|
||||
cy.wait('@Assets');
|
||||
cy.wait('@Accounts');
|
||||
|
||||
cy.mockVegaWalletTransaction();
|
||||
});
|
||||
|
||||
it('transfer fees tooltips', () => {
|
||||
// 1003-TRAN-015
|
||||
// 1003-TRAN-016
|
||||
// 1003-TRAN-017
|
||||
// 1003-TRAN-018
|
||||
// 1003-TRAN-019
|
||||
cy.getByTestId(transferForm);
|
||||
cy.contains('Enter manually').click();
|
||||
cy.getByTestId(transferForm)
|
||||
.find(toAddressField)
|
||||
.type(
|
||||
'7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535'
|
||||
);
|
||||
selectAsset(ASSET_SEPOLIA_TBTC);
|
||||
cy.getByTestId(transferForm)
|
||||
.find(amountField)
|
||||
.type('1', { delay: 100, force: true });
|
||||
|
||||
/// Check Include Transfer Fee tooltip
|
||||
cy.get('label[for="include-transfer-fee"] div').realHover();
|
||||
cy.get('[data-side="bottom"] div')
|
||||
.should('be.visible')
|
||||
.should('not.be.empty');
|
||||
|
||||
//Check Transfer Fee tooltip
|
||||
cy.contains('div', 'Transfer fee').realHover();
|
||||
cy.get('[data-side="bottom"] div')
|
||||
.should('be.visible')
|
||||
.should('not.be.empty');
|
||||
|
||||
//Check Amount to be transferred tooltip
|
||||
cy.contains('div', 'Amount to be transferred').realHover();
|
||||
cy.get('[data-side="bottom"] div')
|
||||
.should('be.visible')
|
||||
.should('not.be.empty');
|
||||
|
||||
//Check Total amount (with fee) tooltip
|
||||
cy.contains('div', 'Total amount (with fee)').realHover();
|
||||
cy.get('[data-side="bottom"] div')
|
||||
.should('be.visible')
|
||||
.should('not.be.empty');
|
||||
});
|
||||
|
||||
it('transfer fees', () => {
|
||||
// 1003-TRAN-020
|
||||
// 1003-TRAN-021
|
||||
// 1003-TRAN-022
|
||||
// 1003-TRAN-023
|
||||
cy.getByTestId(transferForm);
|
||||
cy.contains('Enter manually').click();
|
||||
|
||||
cy.getByTestId(transferForm)
|
||||
.find(toAddressField)
|
||||
.type(
|
||||
'7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535'
|
||||
);
|
||||
selectAsset(ASSET_SEPOLIA_TBTC);
|
||||
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
|
||||
|
||||
cy.getByTestId(transferForm)
|
||||
.find(amountField)
|
||||
.type('1', { delay: 100, force: true });
|
||||
|
||||
cy.getByTestId(transferFee)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '0.01');
|
||||
cy.getByTestId(transferAmount)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '1.00');
|
||||
cy.getByTestId(totalTransferfee)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '1.01');
|
||||
cy.getByTestId(includeTransferFeeRadioBtn).click();
|
||||
cy.getByTestId(transferFee)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '0.01');
|
||||
cy.getByTestId(transferAmount)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '0.99');
|
||||
cy.getByTestId(totalTransferfee)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '1.00');
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,131 @@
|
||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||
import { selectAsset } from '../support/helpers';
|
||||
|
||||
const formFieldError = 'input-error-text';
|
||||
const toAddressField = 'input[name="to"]';
|
||||
const amountField = 'input[name="amount"]';
|
||||
const useMaximumAmount = 'use-maximum';
|
||||
const submitWithdrawBtn = 'submit-withdrawal';
|
||||
const ethAddressValue = Cypress.env('ETHEREUM_WALLET_ADDRESS');
|
||||
|
||||
const ASSET_SEPOLIA_TBTC = 2;
|
||||
const ASSET_EURO = 1;
|
||||
|
||||
describe('withdraw form validation', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.mockWeb3Provider();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.setVegaWallet();
|
||||
|
||||
cy.visit('/#/portfolio');
|
||||
cy.getByTestId('Withdrawals').click();
|
||||
|
||||
cy.getByTestId('Withdraw').click(); // sidebar item
|
||||
|
||||
// It also requires connection Ethereum wallet
|
||||
connectEthereumWallet('MetaMask');
|
||||
|
||||
cy.wait('@Accounts');
|
||||
cy.wait('@Assets');
|
||||
});
|
||||
|
||||
it('empty fields', () => {
|
||||
cy.getByTestId(submitWithdrawBtn).click();
|
||||
|
||||
cy.getByTestId(formFieldError).should('contain.text', 'Required');
|
||||
// only 2 despite 3 fields because the ethereum address will be auto populated
|
||||
cy.getByTestId(formFieldError).should('have.length', 2);
|
||||
|
||||
// Test for Ethereum address
|
||||
cy.get(toAddressField).should('have.value', ethAddressValue);
|
||||
});
|
||||
it('min amount', () => {
|
||||
// 1002-WITH-010
|
||||
selectAsset(ASSET_SEPOLIA_TBTC);
|
||||
cy.get(amountField).clear().type('0');
|
||||
cy.getByTestId(submitWithdrawBtn).click();
|
||||
cy.get('[data-testid="input-error-text"]').should(
|
||||
'contain.text',
|
||||
'Value is below minimum'
|
||||
);
|
||||
});
|
||||
it('max amount', () => {
|
||||
// 1002-WITH-005
|
||||
// 1002-WITH-008
|
||||
selectAsset(ASSET_EURO); // Will be above maximum because the vega wallet doesn't have any collateral
|
||||
cy.get(amountField).clear().type('1001', { delay: 100 });
|
||||
cy.getByTestId(submitWithdrawBtn).click();
|
||||
cy.get('[data-testid="input-error-text"]').should(
|
||||
'contain.text',
|
||||
'Insufficient amount in account'
|
||||
);
|
||||
});
|
||||
|
||||
it('can set amount using use maximum button', () => {
|
||||
// 1002-WITH-004
|
||||
selectAsset(ASSET_SEPOLIA_TBTC);
|
||||
cy.getByTestId(useMaximumAmount).click();
|
||||
cy.get(amountField).should('have.value', '1000.00001');
|
||||
});
|
||||
});
|
||||
|
||||
describe(
|
||||
'withdraw actions',
|
||||
{ tags: '@regression', testIsolation: true },
|
||||
() => {
|
||||
// this is extremely ugly hack, but setting it properly in contract is too much effort for such simple validation
|
||||
|
||||
// 1002-WITH-018
|
||||
|
||||
const withdrawalThreshold =
|
||||
Cypress.env('VEGA_ENV') === 'CUSTOM' ? '0.00' : '100.00';
|
||||
before(() => {
|
||||
cy.mockWeb3Provider();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.setVegaWallet();
|
||||
|
||||
cy.visit('/#/portfolio');
|
||||
|
||||
cy.wait('@Accounts');
|
||||
cy.wait('@Assets');
|
||||
|
||||
cy.getByTestId('Withdrawals').click();
|
||||
|
||||
cy.getByTestId('Withdraw').click();
|
||||
|
||||
// It also requires connection Ethereum wallet
|
||||
connectEthereumWallet('MetaMask');
|
||||
|
||||
cy.mockVegaWalletTransaction();
|
||||
});
|
||||
|
||||
it('triggers transaction when submitted', () => {
|
||||
// 1002-WITH-002
|
||||
// 1002-WITH-003
|
||||
selectAsset(ASSET_SEPOLIA_TBTC);
|
||||
cy.getByTestId('BALANCE_AVAILABLE_label').should(
|
||||
'contain.text',
|
||||
'Balance available'
|
||||
);
|
||||
cy.getByTestId('BALANCE_AVAILABLE_value').should(
|
||||
'have.text',
|
||||
'1,000.00001'
|
||||
);
|
||||
cy.getByTestId('WITHDRAWAL_THRESHOLD_label').should(
|
||||
'contain.text',
|
||||
'Delayed withdrawal threshold'
|
||||
);
|
||||
cy.getByTestId('WITHDRAWAL_THRESHOLD_value').should(
|
||||
'contain.text',
|
||||
withdrawalThreshold
|
||||
);
|
||||
cy.getByTestId('DELAY_TIME_label').should('contain.text', 'Delay time');
|
||||
cy.getByTestId('DELAY_TIME_value').should('have.text', 'None');
|
||||
cy.get(amountField).clear().type('10');
|
||||
cy.getByTestId(submitWithdrawBtn).click();
|
||||
cy.getByTestId('toast').should('contain.text', 'Awaiting confirmation');
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -30,11 +30,11 @@ import {
|
||||
blockStatisticsQuery,
|
||||
networkParamQuery,
|
||||
liquidityProvisionsQuery,
|
||||
liquidityProviderFeeShareQuery,
|
||||
successorMarketQuery,
|
||||
parentMarketIdQuery,
|
||||
successorMarketIdsQuery,
|
||||
successorMarketProposalDetailsQuery,
|
||||
liquidityProvidersQuery,
|
||||
} from '@vegaprotocol/mock';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/markets';
|
||||
@@ -162,7 +162,11 @@ const mockTradingPage = (
|
||||
aliasGQLQuery(req, 'Trades', tradesQuery());
|
||||
aliasGQLQuery(req, 'Chart', chartQuery());
|
||||
aliasGQLQuery(req, 'LiquidityProvisions', liquidityProvisionsQuery());
|
||||
aliasGQLQuery(req, 'LiquidityProviders', liquidityProvidersQuery());
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
'LiquidityProviderFeeShare',
|
||||
liquidityProviderFeeShareQuery
|
||||
);
|
||||
aliasGQLQuery(req, 'Candles', candlesQuery());
|
||||
aliasGQLQuery(req, 'Withdrawals', withdrawalsQuery());
|
||||
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
|
||||
|
||||
@@ -12,11 +12,12 @@ NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
||||
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
|
||||
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
|
||||
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
||||
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
@@ -25,3 +26,6 @@ NX_ICEBERG_ORDERS=true
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { FeesContainer } from '../../components/fees-container';
|
||||
|
||||
export const Fees = () => {
|
||||
return (
|
||||
<div className="container p-4 mx-auto">
|
||||
<h1 className="px-4 pb-4 text-2xl">{t('Fees')}</h1>
|
||||
<FeesContainer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { Fees } from './fees';
|
||||
@@ -319,8 +319,7 @@ describe('Closed', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('successor marked should be visible', async () => {
|
||||
it('successor marked should be visible', async () => {
|
||||
const marketsWithSuccessorID = [
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
|
||||
@@ -32,7 +32,7 @@ const WithdrawalsIndicator = () => {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span className="p-1 leading-none rounded bg-vega-clight-500 dark:bg-vega-cdark-500 text-default">
|
||||
<span className="bg-vega-clight-500 dark:bg-vega-cdark-500 text-default rounded p-1 leading-none">
|
||||
{ready.length}
|
||||
</span>
|
||||
);
|
||||
@@ -128,7 +128,7 @@ interface PortfolioGridChildProps {
|
||||
const PortfolioGridChild = ({ children }: PortfolioGridChildProps) => {
|
||||
return (
|
||||
<section className="h-full p-1">
|
||||
<div className="h-full border rounded-sm border-default">{children}</div>
|
||||
<div className="border border-default h-full rounded-sm">{children}</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,44 +1,21 @@
|
||||
import {
|
||||
Input,
|
||||
InputError,
|
||||
Loader,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { FieldValues } from 'react-hook-form';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import classNames from 'classnames';
|
||||
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
|
||||
import { Navigate, useSearchParams } from 'react-router-dom';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { RainbowButton } from './buttons';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { Button } from './buttons';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useReferral } from './hooks/use-referral';
|
||||
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;
|
||||
|
||||
const validateCode = (value: string) => {
|
||||
const number = +`0x${value}`;
|
||||
if (!value || value.length !== 64) {
|
||||
return t('Code must be 64 characters in length');
|
||||
} else if (Number.isNaN(number)) {
|
||||
return t('Code must be be valid hex');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const ApplyCodeForm = () => {
|
||||
const program = useReferralProgram();
|
||||
const navigate = useNavigate();
|
||||
const openWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
|
||||
const [status, setStatus] = useState<
|
||||
'requested' | 'failed' | 'successful' | null
|
||||
>(null);
|
||||
@@ -50,17 +27,11 @@ export const ApplyCodeForm = () => {
|
||||
formState: { errors },
|
||||
setValue,
|
||||
setError,
|
||||
watch,
|
||||
} = useForm();
|
||||
const [params] = useSearchParams();
|
||||
|
||||
const { data: referee } = useReferral({ pubKey, role: 'referee' });
|
||||
const { data: referrer } = useReferral({ pubKey, role: 'referrer' });
|
||||
|
||||
const codeField = watch('code');
|
||||
const { data: previewData, loading: previewLoading } = useReferral({
|
||||
code: validateCode(codeField) ? codeField : undefined,
|
||||
});
|
||||
const { data: referee } = useReferral(pubKey, 'referee');
|
||||
const { data: referrer } = useReferral(pubKey, 'referrer');
|
||||
|
||||
useEffect(() => {
|
||||
const code = params.get('code');
|
||||
@@ -83,7 +54,7 @@ export const ApplyCodeForm = () => {
|
||||
if (!res) {
|
||||
setError('code', {
|
||||
type: 'required',
|
||||
message: t('The transaction could not be sent'),
|
||||
message: 'The transaction could not be sent',
|
||||
});
|
||||
}
|
||||
if (res) {
|
||||
@@ -94,13 +65,9 @@ export const ApplyCodeForm = () => {
|
||||
if (err.message.includes('user rejected')) {
|
||||
setStatus(null);
|
||||
} else {
|
||||
setStatus(null);
|
||||
setError('code', {
|
||||
type: 'required',
|
||||
message:
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: t('Your code has been rejected'),
|
||||
message: 'Your code has been rejected',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -132,21 +99,10 @@ export const ApplyCodeForm = () => {
|
||||
}),
|
||||
});
|
||||
|
||||
// go to main page when successfully applied
|
||||
useEffect(() => {
|
||||
if (status === 'successful') {
|
||||
setTimeout(() => {
|
||||
navigate(Routes.REFERRALS);
|
||||
}, RELOAD_DELAY);
|
||||
}
|
||||
}, [navigate, status]);
|
||||
|
||||
// go to main page if the current pubkey is already a referrer or referee
|
||||
if (referee || referrer) {
|
||||
return <Navigate to={Routes.REFERRALS} />;
|
||||
}
|
||||
|
||||
// show "code applied" message when successfully applied
|
||||
if (status === 'successful') {
|
||||
return (
|
||||
<div className="w-1/2 mx-auto">
|
||||
@@ -154,94 +110,61 @@ export const ApplyCodeForm = () => {
|
||||
<span className="text-vega-green-500">
|
||||
<VegaIcon name={VegaIconNames.TICK} size={20} />
|
||||
</span>{' '}
|
||||
<span className="pt-1">{t('Code applied')}</span>
|
||||
<span className="pt-1">Code applied</span>
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getButtonProps = () => {
|
||||
if (!pubKey) {
|
||||
return {
|
||||
disabled: false,
|
||||
children: t('Connect wallet'),
|
||||
type: 'button' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
|
||||
onClick: ((event) => {
|
||||
event.preventDefault();
|
||||
openWalletDialog();
|
||||
}) as MouseEventHandler,
|
||||
};
|
||||
}
|
||||
|
||||
if (isReadOnly) {
|
||||
if (isReadOnly || !pubKey) {
|
||||
return {
|
||||
disabled: true,
|
||||
children: t('Apply a code'),
|
||||
type: 'submit' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
|
||||
children: 'Apply',
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 'requested') {
|
||||
return {
|
||||
disabled: true,
|
||||
children: t('Confirm in wallet...'),
|
||||
type: 'submit' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
|
||||
children: 'Confirm in wallet...',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
children: t('Apply a code'),
|
||||
type: 'submit' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
|
||||
children: 'Apply',
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg">
|
||||
<h3 className="mb-4 text-2xl text-center calt">
|
||||
{t('Apply a referral code')}
|
||||
</h3>
|
||||
<p className="mb-4 text-center text-base">
|
||||
{t('Enter a referral code to get trading discounts.')}
|
||||
</p>
|
||||
<form
|
||||
className={classNames('w-full flex flex-col gap-4', {
|
||||
'animate-shake': Boolean(errors.code),
|
||||
})}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<label>
|
||||
<span className="sr-only">{t('Your referral code')}</span>
|
||||
<Input
|
||||
hasError={Boolean(errors.code)}
|
||||
{...register('code', {
|
||||
required: t('You have to provide a code to apply it.'),
|
||||
validate: validateCode,
|
||||
})}
|
||||
placeholder="Enter a code"
|
||||
className="mb-2 bg-vega-clight-900 dark:bg-vega-cdark-700"
|
||||
/>
|
||||
</label>
|
||||
<RainbowButton variant="border" {...getButtonProps()} />
|
||||
</form>
|
||||
{errors.code && (
|
||||
<InputError className="break-words overflow-auto">
|
||||
{errors.code.message?.toString()}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
{previewLoading && !previewData ? (
|
||||
<div className="mt-10">
|
||||
<Loader />
|
||||
</div>
|
||||
) : null}
|
||||
{previewData ? (
|
||||
<div className="mt-10">
|
||||
<h2 className="text-2xl mb-5">{t('You are joining')}</h2>
|
||||
<Statistics data={previewData} program={program} as="referee" />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
<div className="w-1/2 mx-auto">
|
||||
<h3 className="mb-5 text-xl text-center uppercase calt">
|
||||
Apply a referral code
|
||||
</h3>
|
||||
<p className="mb-6 text-center">Enter a referral code</p>
|
||||
<form
|
||||
className={classNames('w-full flex flex-col gap-3', {
|
||||
'animate-shake': Boolean(errors.code),
|
||||
})}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<label className="flex-grow">
|
||||
<span className="block mb-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
Your referral code
|
||||
</span>
|
||||
<Input
|
||||
hasError={Boolean(errors.code)}
|
||||
{...register('code', {
|
||||
required: 'You have to provide a code to apply it.',
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<Button className="w-full" type="submit" {...getButtonProps()} />
|
||||
</form>
|
||||
{errors.code && (
|
||||
<InputError>{errors.code.message?.toString()}</InputError>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,23 +16,20 @@ export const RainbowButton = ({
|
||||
}: RainbowButtonProps & ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button
|
||||
className={classNames(
|
||||
'bg-rainbow rounded-lg overflow-hidden disabled:opacity-40',
|
||||
'hover:bg-rainbow-180 hover:animate-spin-rainbow',
|
||||
'bg-rainbow hover:bg-none hover:bg-rainbow enabled:hover:bg-vega-pink-500 rounded-lg overflow-hidden disabled:opacity-40',
|
||||
{
|
||||
'px-5 py-3 text-white': variant === 'full',
|
||||
'p-[0.125rem]': variant === 'border',
|
||||
}
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
{
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white px-5 py-3 rounded-[0.35rem] overflow-hidden':
|
||||
variant === 'border',
|
||||
},
|
||||
className
|
||||
)}
|
||||
className={classNames({
|
||||
'bg-white dark:bg-vega-cdark-900 text-black dark:text-white px-5 py-3 rounded-[0.35rem] overflow-hidden':
|
||||
variant === 'border',
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
@@ -58,20 +55,6 @@ const DISABLED_RAINBOW_TAB_STYLE = classNames(
|
||||
'[&.active]:text-white'
|
||||
);
|
||||
|
||||
const TAB_STYLE = classNames(
|
||||
'inline-block',
|
||||
'bg-transparent',
|
||||
'text-vega-clight-200 dark:text-vega-cdark-200',
|
||||
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100',
|
||||
'data-[state="active"]:text-black dark:data-[state="active"]:text-white',
|
||||
'data-[state="active"]:border-b-2 data-[state="active"]:border-b-black dark:data-[state="active"]:border-b-white',
|
||||
'[&.active]:text-black dark:[&.active]:text-white',
|
||||
'[&.active]:border-b-2 [&.active]:border-b-black dark:[&.active]:border-b-white',
|
||||
'mx-4 px-0 py-3',
|
||||
'uppercase'
|
||||
);
|
||||
const DISABLED_TAB_STYLE = classNames('pointer-events-none');
|
||||
|
||||
export const RainbowTabButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
{ disabled?: boolean } & ButtonHTMLAttributes<HTMLButtonElement>
|
||||
@@ -110,26 +93,6 @@ export const RainbowTabLink = ({
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
export const TabLink = ({
|
||||
to,
|
||||
children,
|
||||
className,
|
||||
disabled = false,
|
||||
...props
|
||||
}: { disabled?: boolean } & ComponentProps<typeof NavLink>) => (
|
||||
<NavLink
|
||||
to={to}
|
||||
className={classNames(
|
||||
TAB_STYLE,
|
||||
disabled && DISABLED_TAB_STYLE,
|
||||
typeof className === 'string' ? className : undefined
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
export const Button = forwardRef<
|
||||
HTMLButtonElement,
|
||||
ComponentProps<typeof TradingButton>
|
||||
|
||||
@@ -4,8 +4,3 @@ export const GRADIENT =
|
||||
|
||||
export const SKY_BACKGROUND =
|
||||
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[40%_0px] bg-[length:1440px] bg-no-repeat bg-local';
|
||||
|
||||
// TODO: Update the links to use the correct referral related pages
|
||||
export const REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
|
||||
export const ABOUT_REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
|
||||
export const DISCLAIMER_REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
|
||||
|
||||
@@ -19,58 +19,70 @@ import {
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
|
||||
import { useStakeAvailable } from './hooks/use-stake-available';
|
||||
import {
|
||||
ABOUT_REFERRAL_DOCS_LINK,
|
||||
DISCLAIMER_REFERRAL_DOCS_LINK,
|
||||
} from './constants';
|
||||
import { useReferral } from './hooks/use-referral';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export const CreateCodeContainer = () => {
|
||||
return <CreateCodeForm />;
|
||||
const { stakeAvailable, requiredStake } = useStakeAvailable();
|
||||
if (stakeAvailable == null || requiredStake == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CreateCodeForm
|
||||
currentStakeAvailable={stakeAvailable}
|
||||
requiredStake={requiredStake}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CreateCodeForm = () => {
|
||||
export const CreateCodeForm = ({
|
||||
currentStakeAvailable,
|
||||
requiredStake,
|
||||
}: {
|
||||
currentStakeAvailable: bigint;
|
||||
requiredStake: bigint;
|
||||
}) => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const openWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
return (
|
||||
<div className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg">
|
||||
<h3 className="mb-4 text-2xl text-center calt">
|
||||
{t('Create a referral code')}
|
||||
<div className="w-1/2 mx-auto">
|
||||
<h3 className="mb-5 text-xl text-center uppercase calt">
|
||||
Create a referral code
|
||||
</h3>
|
||||
<p className="mb-4 text-center text-base">
|
||||
{t(
|
||||
'Generate a referral code to share with your friends and start earning commission.'
|
||||
)}
|
||||
<p className="mb-6 text-center">
|
||||
Generate a referral code to share with your friends and start earning
|
||||
commission.
|
||||
</p>
|
||||
|
||||
<div className="w-full flex flex-col">
|
||||
<RainbowButton
|
||||
variant="border"
|
||||
disabled={isReadOnly}
|
||||
onClick={() => {
|
||||
if (pubKey) {
|
||||
setDialogOpen(true);
|
||||
} else {
|
||||
openWalletDialog();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{pubKey ? t('Create a referral code') : t('Connect wallet')}
|
||||
</RainbowButton>
|
||||
<div className="mb-5">
|
||||
<div className="text-center">
|
||||
<RainbowButton
|
||||
variant="border"
|
||||
onClick={() => {
|
||||
if (pubKey) {
|
||||
setDialogOpen(true);
|
||||
} else {
|
||||
openWalletDialog();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{pubKey ? 'Create a referral code' : 'Connect wallet'}
|
||||
</RainbowButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
title={t('Create a referral code')}
|
||||
title="Create a referral code"
|
||||
open={dialogOpen}
|
||||
onChange={() => setDialogOpen(false)}
|
||||
size="small"
|
||||
>
|
||||
<CreateCodeDialog setDialogOpen={setDialogOpen} />
|
||||
<CreateCodeDialog
|
||||
currentStakeAvailable={currentStakeAvailable}
|
||||
setDialogOpen={setDialogOpen}
|
||||
requiredStake={requiredStake}
|
||||
/>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
@@ -78,21 +90,21 @@ export const CreateCodeForm = () => {
|
||||
|
||||
const CreateCodeDialog = ({
|
||||
setDialogOpen,
|
||||
currentStakeAvailable,
|
||||
requiredStake,
|
||||
}: {
|
||||
setDialogOpen: (open: boolean) => void;
|
||||
currentStakeAvailable: bigint;
|
||||
requiredStake: bigint;
|
||||
}) => {
|
||||
const createLink = useLinks(DApp.Governance);
|
||||
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
|
||||
const { refetch } = useReferral({ pubKey, role: 'referrer' });
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const [code, setCode] = useState<string | null>(null);
|
||||
const [status, setStatus] = useState<
|
||||
'idle' | 'loading' | 'success' | 'error'
|
||||
>('idle');
|
||||
|
||||
const { stakeAvailable: currentStakeAvailable, requiredStake } =
|
||||
useStakeAvailable();
|
||||
|
||||
const onSubmit = () => {
|
||||
if (isReadOnly || !pubKey) {
|
||||
setErr('Not connected');
|
||||
@@ -128,60 +140,45 @@ const CreateCodeDialog = ({
|
||||
const getButtonProps = () => {
|
||||
if (status === 'idle' || status === 'error') {
|
||||
return {
|
||||
children: t('Generate code'),
|
||||
children: 'Generate code',
|
||||
onClick: () => onSubmit(),
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 'loading') {
|
||||
return {
|
||||
children: t('Confirm in wallet...'),
|
||||
children: 'Confirm in wallet...',
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 'success') {
|
||||
return {
|
||||
children: t('Close'),
|
||||
children: 'Close',
|
||||
intent: Intent.Success,
|
||||
onClick: () => {
|
||||
refetch();
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onClick: () => setDialogOpen(false),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if (!pubKey || currentStakeAvailable == null || requiredStake == null) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>{t('You must be connected to the Vega wallet.')}</p>
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={() => setDialogOpen(false)}
|
||||
>
|
||||
{t('Close')}
|
||||
</TradingButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentStakeAvailable < requiredStake) {
|
||||
// TODO: Add when network parameters are updated
|
||||
if (
|
||||
currentStakeAvailable === BigInt(0) ||
|
||||
currentStakeAvailable < requiredStake
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>
|
||||
{t('You need at least')}{' '}
|
||||
{addDecimalsFormatNumber(requiredStake.toString(), 18)}{' '}
|
||||
{t(
|
||||
'VEGA staked to generate a referral code and participate in the referral program.'
|
||||
)}
|
||||
You need at least{' '}
|
||||
{addDecimalsFormatNumber(requiredStake.toString(), 18)} VEGA staked to
|
||||
generate a referral code and participate in the referral program.
|
||||
</p>
|
||||
<TradingAnchorButton
|
||||
href={createLink(TokenStaticLinks.ASSOCIATE)}
|
||||
intent={Intent.Primary}
|
||||
target="_blank"
|
||||
>
|
||||
{t('Stake some $VEGA now')}
|
||||
Stake some $VEGA now
|
||||
</TradingAnchorButton>
|
||||
</div>
|
||||
);
|
||||
@@ -191,9 +188,8 @@ const CreateCodeDialog = ({
|
||||
<div className="flex flex-col gap-4">
|
||||
{(status === 'idle' || status === 'loading' || status === 'error') && (
|
||||
<p>
|
||||
{t(
|
||||
'Generate a referral code to share with your friends and start earning commission.'
|
||||
)}
|
||||
Generate a referral code to share with your friends and start earning
|
||||
commission.
|
||||
</p>
|
||||
)}
|
||||
{status === 'success' && code && (
|
||||
@@ -208,7 +204,7 @@ const CreateCodeDialog = ({
|
||||
className="text-sm no-underline"
|
||||
icon={<VegaIcon name={VegaIconNames.COPY} />}
|
||||
>
|
||||
<span>{t('Copy')}</span>
|
||||
<span>Copy</span>
|
||||
</TradingButton>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
@@ -219,13 +215,10 @@ const CreateCodeDialog = ({
|
||||
{...getButtonProps()}
|
||||
/>
|
||||
{err && <InputError>{err}</InputError>}
|
||||
{/* TODO: Add links */}
|
||||
<div className="flex justify-center pt-5 mt-2 text-sm border-t gap-4 text-default border-default">
|
||||
<ExternalLink href={ABOUT_REFERRAL_DOCS_LINK}>
|
||||
{t('About the referral program')}
|
||||
</ExternalLink>
|
||||
<ExternalLink href={DISCLAIMER_REFERRAL_DOCS_LINK}>
|
||||
{t('Disclaimer')}
|
||||
</ExternalLink>
|
||||
<ExternalLink>About the referral program</ExternalLink>
|
||||
<ExternalLink>Disclaimer</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { RainbowButton } from './buttons';
|
||||
import { AnimatedDudeWithWire } from './graphics/dude';
|
||||
import { LayoutWithSky } from './layout';
|
||||
import { Routes } from '../../lib/links';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export const ErrorBoundary = () => {
|
||||
const error = useRouteError();
|
||||
@@ -40,7 +39,7 @@ export const ErrorBoundary = () => {
|
||||
variant="border"
|
||||
className="text-xs"
|
||||
>
|
||||
{t('Go back and try again')}
|
||||
Go back and try again
|
||||
</RainbowButton>
|
||||
</p>
|
||||
</LayoutWithSky>
|
||||
@@ -61,7 +60,7 @@ export const NotFound = () => {
|
||||
<h1 className="text-6xl font-alpha calt mb-10">{'Not found'}</h1>
|
||||
|
||||
<p className="text-lg mb-10">
|
||||
{t("The page you're looking for doesn't exists.")}
|
||||
{"The page you're looking for doesn't exists."}
|
||||
</p>
|
||||
|
||||
<p className="text-lg mb-10">
|
||||
@@ -70,7 +69,7 @@ export const NotFound = () => {
|
||||
variant="border"
|
||||
className="text-xs"
|
||||
>
|
||||
{t('Go back and try again')}
|
||||
Go back and try again
|
||||
</RainbowButton>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
query ReferralProgram {
|
||||
currentReferralProgram {
|
||||
id
|
||||
version
|
||||
endOfProgramTimestamp
|
||||
windowLength
|
||||
endedAt
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
query CurrentEpochInfo {
|
||||
epoch {
|
||||
id
|
||||
timestamps {
|
||||
start
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
query Referees($code: ID!, $aggregationEpochs: Int) {
|
||||
referralSetReferees(id: $code, aggregationEpochs: $aggregationEpochs) {
|
||||
edges {
|
||||
node {
|
||||
referralSetId
|
||||
refereeId
|
||||
joinedAt
|
||||
atEpoch
|
||||
totalRefereeNotionalTakerVolume
|
||||
totalRefereeGeneratedRewards
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
query ReferralSetStats($code: ID!, $epoch: Int) {
|
||||
referralSetStats(setId: $code, epoch: $epoch) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
partyId
|
||||
discountFactor
|
||||
rewardFactor
|
||||
epochNotionalTakerVolume
|
||||
referralSetRunningNotionalTakerVolume
|
||||
rewardsMultiplier
|
||||
rewardsFactorMultiplier
|
||||
referrerTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
query ReferralSets($id: ID, $referrer: ID, $referee: ID) {
|
||||
referralSets(id: $id, referrer: $referrer, referee: $referee) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ReferralProgramQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ReferralProgramQuery = { __typename?: 'Query', currentReferralProgram?: { __typename?: 'CurrentReferralProgram', id: string, version: number, endOfProgramTimestamp: any, windowLength: number, endedAt?: any | null, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | null };
|
||||
|
||||
|
||||
export const ReferralProgramDocument = gql`
|
||||
query ReferralProgram {
|
||||
currentReferralProgram {
|
||||
id
|
||||
version
|
||||
endOfProgramTimestamp
|
||||
windowLength
|
||||
endedAt
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useReferralProgramQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useReferralProgramQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useReferralProgramQuery` 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 } = useReferralProgramQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useReferralProgramQuery(baseOptions?: Apollo.QueryHookOptions<ReferralProgramQuery, ReferralProgramQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ReferralProgramQuery, ReferralProgramQueryVariables>(ReferralProgramDocument, options);
|
||||
}
|
||||
export function useReferralProgramLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ReferralProgramQuery, ReferralProgramQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ReferralProgramQuery, ReferralProgramQueryVariables>(ReferralProgramDocument, options);
|
||||
}
|
||||
export type ReferralProgramQueryHookResult = ReturnType<typeof useReferralProgramQuery>;
|
||||
export type ReferralProgramLazyQueryHookResult = ReturnType<typeof useReferralProgramLazyQuery>;
|
||||
export type ReferralProgramQueryResult = Apollo.QueryResult<ReferralProgramQuery, ReferralProgramQueryVariables>;
|
||||
@@ -1,49 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type CurrentEpochInfoQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type CurrentEpochInfoQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null } } };
|
||||
|
||||
|
||||
export const CurrentEpochInfoDocument = gql`
|
||||
query CurrentEpochInfo {
|
||||
epoch {
|
||||
id
|
||||
timestamps {
|
||||
start
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useCurrentEpochInfoQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useCurrentEpochInfoQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useCurrentEpochInfoQuery` 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 } = useCurrentEpochInfoQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCurrentEpochInfoQuery(baseOptions?: Apollo.QueryHookOptions<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>(CurrentEpochInfoDocument, options);
|
||||
}
|
||||
export function useCurrentEpochInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>(CurrentEpochInfoDocument, options);
|
||||
}
|
||||
export type CurrentEpochInfoQueryHookResult = ReturnType<typeof useCurrentEpochInfoQuery>;
|
||||
export type CurrentEpochInfoLazyQueryHookResult = ReturnType<typeof useCurrentEpochInfoLazyQuery>;
|
||||
export type CurrentEpochInfoQueryResult = Apollo.QueryResult<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>;
|
||||
@@ -1,59 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
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']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type RefereesQuery = { __typename?: 'Query', referralSetReferees: { __typename?: 'ReferralSetRefereeConnection', edges: Array<{ __typename?: 'ReferralSetRefereeEdge', node: { __typename?: 'ReferralSetReferee', referralSetId: string, refereeId: string, joinedAt: any, atEpoch: number, totalRefereeNotionalTakerVolume: string, totalRefereeGeneratedRewards: string } } | null> } };
|
||||
|
||||
|
||||
export const RefereesDocument = gql`
|
||||
query Referees($code: ID!, $aggregationEpochs: Int) {
|
||||
referralSetReferees(id: $code, aggregationEpochs: $aggregationEpochs) {
|
||||
edges {
|
||||
node {
|
||||
referralSetId
|
||||
refereeId
|
||||
joinedAt
|
||||
atEpoch
|
||||
totalRefereeNotionalTakerVolume
|
||||
totalRefereeGeneratedRewards
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useRefereesQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useRefereesQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useRefereesQuery` 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 } = useRefereesQuery({
|
||||
* variables: {
|
||||
* code: // value for 'code'
|
||||
* aggregationEpochs: // value for 'aggregationEpochs'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useRefereesQuery(baseOptions: Apollo.QueryHookOptions<RefereesQuery, RefereesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<RefereesQuery, RefereesQueryVariables>(RefereesDocument, options);
|
||||
}
|
||||
export function useRefereesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RefereesQuery, RefereesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<RefereesQuery, RefereesQueryVariables>(RefereesDocument, options);
|
||||
}
|
||||
export type RefereesQueryHookResult = ReturnType<typeof useRefereesQuery>;
|
||||
export type RefereesLazyQueryHookResult = ReturnType<typeof useRefereesLazyQuery>;
|
||||
export type RefereesQueryResult = Apollo.QueryResult<RefereesQuery, RefereesQueryVariables>;
|
||||
@@ -1,62 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ReferralSetStatsQueryVariables = Types.Exact<{
|
||||
code: Types.Scalars['ID'];
|
||||
epoch?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
}>;
|
||||
|
||||
|
||||
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 const ReferralSetStatsDocument = gql`
|
||||
query ReferralSetStats($code: ID!, $epoch: Int) {
|
||||
referralSetStats(setId: $code, epoch: $epoch) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
partyId
|
||||
discountFactor
|
||||
rewardFactor
|
||||
epochNotionalTakerVolume
|
||||
referralSetRunningNotionalTakerVolume
|
||||
rewardsMultiplier
|
||||
rewardsFactorMultiplier
|
||||
referrerTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useReferralSetStatsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useReferralSetStatsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useReferralSetStatsQuery` 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 } = useReferralSetStatsQuery({
|
||||
* variables: {
|
||||
* code: // value for 'code'
|
||||
* epoch: // value for 'epoch'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useReferralSetStatsQuery(baseOptions: Apollo.QueryHookOptions<ReferralSetStatsQuery, ReferralSetStatsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ReferralSetStatsQuery, ReferralSetStatsQueryVariables>(ReferralSetStatsDocument, options);
|
||||
}
|
||||
export function useReferralSetStatsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ReferralSetStatsQuery, ReferralSetStatsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ReferralSetStatsQuery, ReferralSetStatsQueryVariables>(ReferralSetStatsDocument, options);
|
||||
}
|
||||
export type ReferralSetStatsQueryHookResult = ReturnType<typeof useReferralSetStatsQuery>;
|
||||
export type ReferralSetStatsLazyQueryHookResult = ReturnType<typeof useReferralSetStatsLazyQuery>;
|
||||
export type ReferralSetStatsQueryResult = Apollo.QueryResult<ReferralSetStatsQuery, ReferralSetStatsQueryVariables>;
|
||||
@@ -1,59 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ReferralSetsQueryVariables = Types.Exact<{
|
||||
id?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
referrer?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
referee?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type ReferralSetsQuery = { __typename?: 'Query', referralSets: { __typename?: 'ReferralSetConnection', edges: Array<{ __typename?: 'ReferralSetEdge', node: { __typename?: 'ReferralSet', id: string, referrer: string, createdAt: any, updatedAt: any } } | null> } };
|
||||
|
||||
|
||||
export const ReferralSetsDocument = gql`
|
||||
query ReferralSets($id: ID, $referrer: ID, $referee: ID) {
|
||||
referralSets(id: $id, referrer: $referrer, referee: $referee) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useReferralSetsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useReferralSetsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useReferralSetsQuery` 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 } = useReferralSetsQuery({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* referrer: // value for 'referrer'
|
||||
* referee: // value for 'referee'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useReferralSetsQuery(baseOptions?: Apollo.QueryHookOptions<ReferralSetsQuery, ReferralSetsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ReferralSetsQuery, ReferralSetsQueryVariables>(ReferralSetsDocument, options);
|
||||
}
|
||||
export function useReferralSetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ReferralSetsQuery, ReferralSetsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ReferralSetsQuery, ReferralSetsQueryVariables>(ReferralSetsDocument, options);
|
||||
}
|
||||
export type ReferralSetsQueryHookResult = ReturnType<typeof useReferralSetsQuery>;
|
||||
export type ReferralSetsLazyQueryHookResult = ReturnType<typeof useReferralSetsLazyQuery>;
|
||||
export type ReferralSetsQueryResult = Apollo.QueryResult<ReferralSetsQuery, ReferralSetsQueryVariables>;
|
||||
@@ -1,8 +1,32 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { getNumberFormat } from '@vegaprotocol/utils';
|
||||
import { addDays } from 'date-fns';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import omit from 'lodash/omit';
|
||||
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
|
||||
|
||||
// TODO: Generate query
|
||||
// eslint-disable-next-line
|
||||
const REFERRAL_PROGRAM_QUERY = gql`
|
||||
query ReferralProgram {
|
||||
currentReferralProgram {
|
||||
id
|
||||
version
|
||||
endOfProgramTimestamp
|
||||
windowLength
|
||||
endedAt
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const STAKING_TIERS_MAPPING: Record<number, string> = {
|
||||
1: 'Tradestarter',
|
||||
@@ -59,11 +83,11 @@ const MOCK = {
|
||||
};
|
||||
|
||||
export const useReferralProgram = () => {
|
||||
const { data, loading, error } = useReferralProgramQuery({
|
||||
const { data, loading, error } = useQuery(REFERRAL_PROGRAM_QUERY, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
if (!data || !data.currentReferralProgram) {
|
||||
if (!data) {
|
||||
return {
|
||||
benefitTiers: [],
|
||||
stakingTiers: [],
|
||||
@@ -80,15 +104,11 @@ export const useReferralProgram = () => {
|
||||
.map((t, i) => {
|
||||
return {
|
||||
tier: i + 1,
|
||||
rewardFactor: Number(t.referralRewardFactor),
|
||||
commission: Number(t.referralRewardFactor) * 100 + '%',
|
||||
discountFactor: Number(t.referralDiscountFactor),
|
||||
discount: Number(t.referralDiscountFactor) * 100 + '%',
|
||||
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
|
||||
volume: getNumberFormat(0).format(
|
||||
Number(t.minimumRunningNotionalTakerVolume)
|
||||
),
|
||||
epochs: Number(t.minimumEpochs),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -103,16 +123,10 @@ export const useReferralProgram = () => {
|
||||
};
|
||||
});
|
||||
|
||||
const details = omit(
|
||||
data.currentReferralProgram,
|
||||
'benefitTiers',
|
||||
'stakingTiers'
|
||||
);
|
||||
|
||||
return {
|
||||
benefitTiers,
|
||||
stakingTiers,
|
||||
details,
|
||||
details: omit(data.currentReferralProgram, 'benefitTiers', 'stakingTiers'),
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
||||
@@ -1,116 +1,114 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { useCallback } from 'react';
|
||||
import { useRefereesQuery } from './__generated__/Referees';
|
||||
import compact from 'lodash/compact';
|
||||
import type { ReferralSetsQueryVariables } from './__generated__/ReferralSets';
|
||||
import { useReferralSetsQuery } from './__generated__/ReferralSets';
|
||||
|
||||
export const DEFAULT_AGGREGATION_DAYS = 30;
|
||||
const REFERRER_QUERY = gql`
|
||||
query ReferralSets($partyId: ID!) {
|
||||
referralSets(referrer: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export type Role = 'referrer' | 'referee';
|
||||
type UseReferralArgs = (
|
||||
| { code: string }
|
||||
| { pubKey: string | null; role: Role }
|
||||
) & {
|
||||
aggregationEpochs?: number;
|
||||
const REFEREE_QUERY = gql`
|
||||
query ReferralSets($partyId: ID!) {
|
||||
referralSets(referee: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const REFEREES_QUERY = gql`
|
||||
query ReferralSets($code: ID!) {
|
||||
referralSetReferees(id: $code) {
|
||||
edges {
|
||||
node {
|
||||
referralSetId
|
||||
refereeId
|
||||
joinedAt
|
||||
atEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// TODO: generate types after perps work is merged
|
||||
export type ReferralData = {
|
||||
code: string;
|
||||
referees: Array<{
|
||||
refereeId: string;
|
||||
joinedAt: string;
|
||||
atEpoch: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
const prepareVariables = (
|
||||
args: UseReferralArgs
|
||||
): [ReferralSetsQueryVariables, boolean] => {
|
||||
const byCode = 'code' in args;
|
||||
const byRole = 'pubKey' in args && 'role' in args;
|
||||
let variables = {};
|
||||
let skip = true;
|
||||
if (byCode) {
|
||||
variables = {
|
||||
id: args.code,
|
||||
};
|
||||
skip = !args.code;
|
||||
}
|
||||
if (byRole) {
|
||||
if (args.role === 'referee') {
|
||||
variables = { referee: args.pubKey };
|
||||
}
|
||||
if (args.role === 'referrer') {
|
||||
variables = { referrer: args.pubKey };
|
||||
}
|
||||
skip = !args.pubKey;
|
||||
}
|
||||
|
||||
return [variables, skip];
|
||||
};
|
||||
|
||||
export const useReferral = (args: UseReferralArgs) => {
|
||||
const [variables, skip] = prepareVariables(args);
|
||||
export const useReferral = (
|
||||
pubKey: string | null,
|
||||
role: 'referrer' | 'referee'
|
||||
) => {
|
||||
const query = {
|
||||
referrer: REFERRER_QUERY,
|
||||
referee: REFEREE_QUERY,
|
||||
};
|
||||
|
||||
const {
|
||||
data: referralData,
|
||||
loading: referralLoading,
|
||||
error: referralError,
|
||||
refetch: referralRefetch,
|
||||
} = useReferralSetsQuery({
|
||||
variables,
|
||||
skip,
|
||||
} = useQuery(query[role], {
|
||||
variables: {
|
||||
partyId: pubKey,
|
||||
},
|
||||
skip: !pubKey,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
// A user can only have 1 active referral program at a time
|
||||
const referralSet =
|
||||
referralData?.referralSets.edges &&
|
||||
referralData.referralSets.edges.length > 0
|
||||
? referralData.referralSets.edges[0]?.node
|
||||
: undefined;
|
||||
const referral = referralData?.referralSets.edges.length
|
||||
? referralData.referralSets.edges[0].node
|
||||
: undefined;
|
||||
|
||||
const {
|
||||
data: refereesData,
|
||||
loading: refereesLoading,
|
||||
error: refereesError,
|
||||
refetch: refereesRefetch,
|
||||
} = useRefereesQuery({
|
||||
} = useQuery(REFEREES_QUERY, {
|
||||
variables: {
|
||||
code: referralSet?.id as string,
|
||||
aggregationEpochs:
|
||||
args.aggregationEpochs != null
|
||||
? args.aggregationEpochs
|
||||
: DEFAULT_AGGREGATION_DAYS,
|
||||
code: referral?.id,
|
||||
},
|
||||
skip: !referralSet?.id,
|
||||
skip: !referral?.id,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
context: { isEnlargedTimeout: true },
|
||||
});
|
||||
|
||||
const referees = compact(
|
||||
removePaginationWrapper(refereesData?.referralSetReferees.edges)
|
||||
const referees = removePaginationWrapper(
|
||||
refereesData?.referralSetReferees.edges
|
||||
);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
referralRefetch();
|
||||
refereesRefetch();
|
||||
}, [refereesRefetch, referralRefetch]);
|
||||
|
||||
const byReferee =
|
||||
'role' in args && 'pubKey' in args && args.role === 'referee';
|
||||
const referee = byReferee
|
||||
? referees.find((r) => r.refereeId === args.pubKey) || null
|
||||
: null;
|
||||
|
||||
const data =
|
||||
referralSet && refereesData
|
||||
referral && refereesData
|
||||
? {
|
||||
code: referralSet.id,
|
||||
role: 'role' in args ? args.role : null,
|
||||
referee: referee,
|
||||
referrerId: referralSet.referrer,
|
||||
createdAt: referralSet.createdAt,
|
||||
code: referral.id,
|
||||
referees,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
data,
|
||||
data: data as ReferralData | undefined,
|
||||
loading: referralLoading || refereesLoading,
|
||||
error: referralError || refereesError,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Table } from './table';
|
||||
|
||||
export const HowItWorksTable = () => (
|
||||
@@ -14,9 +13,7 @@ export const HowItWorksTable = () => (
|
||||
1
|
||||
</span>
|
||||
),
|
||||
step: t(
|
||||
'Referrers generate a code assigned to their key via an on chain transaction'
|
||||
),
|
||||
step: 'Referrers generate a code assigned to their key via an on chain transaction',
|
||||
},
|
||||
{
|
||||
number: (
|
||||
@@ -24,9 +21,7 @@ export const HowItWorksTable = () => (
|
||||
2
|
||||
</span>
|
||||
),
|
||||
step: t(
|
||||
'Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction'
|
||||
),
|
||||
step: 'Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction',
|
||||
},
|
||||
{
|
||||
number: (
|
||||
@@ -34,9 +29,7 @@ export const HowItWorksTable = () => (
|
||||
3
|
||||
</span>
|
||||
),
|
||||
step: t(
|
||||
'Discounts are applied automatically during trading based on the key(s) used'
|
||||
),
|
||||
step: 'Discounts are applied automatically during trading based on the key(s) used',
|
||||
},
|
||||
{
|
||||
number: (
|
||||
@@ -44,9 +37,7 @@ export const HowItWorksTable = () => (
|
||||
4
|
||||
</span>
|
||||
),
|
||||
step: t(
|
||||
'Referrers earn commission based on a percentage of the taker fees their referees pay'
|
||||
),
|
||||
step: 'Referrers earn commission based on a percentage of the taker fees their referees pay',
|
||||
},
|
||||
{
|
||||
number: (
|
||||
@@ -54,9 +45,7 @@ export const HowItWorksTable = () => (
|
||||
5
|
||||
</span>
|
||||
),
|
||||
step: t(
|
||||
'The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee'
|
||||
),
|
||||
step: 'The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee',
|
||||
},
|
||||
]}
|
||||
></Table>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import { AnimatedDudeWithWire } from './graphics/dude';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export const LandingBanner = () => {
|
||||
return (
|
||||
@@ -8,18 +7,22 @@ export const LandingBanner = () => {
|
||||
<div className="">
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute top-20 right-[120px] md:right-[240px] max-sm:hidden"
|
||||
className="absolute top-64 right-[220px] md:right-[340px] max-sm:hidden"
|
||||
>
|
||||
<AnimatedDudeWithWire />
|
||||
</div>
|
||||
<div className="pt-20 sm:w-[50%]">
|
||||
<div className="pt-32 sm:w-[50%]">
|
||||
<h1 className="text-6xl font-alpha calt mb-10">
|
||||
{t('Earn commission & stake rewards')}
|
||||
Earn commission & stake rewards
|
||||
</h1>
|
||||
<p className="text-lg mb-10">
|
||||
{t(
|
||||
'Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.'
|
||||
)}
|
||||
Invite friends and earn commission in the form of Vega rewards from
|
||||
the trading fees they pay. Stake those rewards to earn multipliers
|
||||
on future rewards.
|
||||
</p>
|
||||
<p className="text-lg">
|
||||
Any friends that join using the code will receive discounts off
|
||||
trading fees.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@ import classNames from 'classnames';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { SKY_BACKGROUND } from './constants';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const Layout = ({
|
||||
className,
|
||||
@@ -29,10 +28,8 @@ export const LayoutWithSky = ({
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<TinyScroll
|
||||
className={classNames('max-h-full overflow-auto', SKY_BACKGROUND)}
|
||||
>
|
||||
<div className={classNames('h-full overflow-auto', SKY_BACKGROUND)}>
|
||||
<Layout className={className} {...props} />
|
||||
</TinyScroll>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,375 +1,113 @@
|
||||
import { CodeTile, StatTile } from './tile';
|
||||
import { Tile } from './tile';
|
||||
import {
|
||||
CopyWithTooltip,
|
||||
Input,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Button, RainbowButton } from './buttons';
|
||||
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { DEFAULT_AGGREGATION_DAYS, useReferral } from './hooks/use-referral';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import type { ReferralData } from './hooks/use-referral';
|
||||
import { useReferral } from './hooks/use-referral';
|
||||
import { CreateCodeContainer } from './create-code-form';
|
||||
import classNames from 'classnames';
|
||||
import { Table } from './table';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
getDateFormat,
|
||||
getDateTimeFormat,
|
||||
getNumberFormat,
|
||||
getUserLocale,
|
||||
removePaginationWrapper,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
|
||||
import compact from 'lodash/compact';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
import { useStakeAvailable } from './hooks/use-stake-available';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
|
||||
const CodeTile = ({
|
||||
code,
|
||||
as,
|
||||
}: {
|
||||
code: string;
|
||||
as: 'referrer' | 'referee';
|
||||
}) => {
|
||||
return (
|
||||
<Tile variant="rainbow">
|
||||
<h3 className="mb-1 text-lg calt">Your referral code</h3>
|
||||
{as === 'referrer' && (
|
||||
<p className="mb-3 text-sm text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
Share this code with friends
|
||||
</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Input size={1} readOnly value={code} />
|
||||
<CopyWithTooltip text={code}>
|
||||
<Button
|
||||
className="text-sm no-underline"
|
||||
icon={<VegaIcon name={VegaIconNames.COPY} />}
|
||||
>
|
||||
<span>Copy</span>
|
||||
</Button>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
</Tile>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReferralStatistics = () => {
|
||||
const openWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
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,
|
||||
});
|
||||
const { data: referee } = useReferral(pubKey, 'referee');
|
||||
const { data: referrer } = useReferral(pubKey, 'referrer');
|
||||
|
||||
if (!pubKey) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<RainbowButton variant="border" onClick={() => openWalletDialog()}>
|
||||
Connect wallet
|
||||
</RainbowButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 />;
|
||||
};
|
||||
|
||||
export const Statistics = ({
|
||||
const Statistics = ({
|
||||
data,
|
||||
program,
|
||||
as,
|
||||
}: {
|
||||
data: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||
program: ReturnType<typeof useReferralProgram>;
|
||||
data: ReferralData;
|
||||
as: 'referrer' | 'referee';
|
||||
}) => {
|
||||
const { benefitTiers, details } = program;
|
||||
const { data: epochData } = useCurrentEpochInfoQuery();
|
||||
const { stakeAvailable } = useStakeAvailable();
|
||||
const { data: statsData } = useReferralSetStatsQuery({
|
||||
variables: {
|
||||
code: data.code,
|
||||
},
|
||||
skip: !data?.code,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
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));
|
||||
const refereeInfo = data.referee;
|
||||
const refereeStats = stats?.find(
|
||||
(r) => r.partyId === data.referee?.refereeId
|
||||
);
|
||||
|
||||
const statsAvailable = stats && stats.length > 0 && stats[0];
|
||||
const baseCommissionValue = statsAvailable
|
||||
? Number(statsAvailable.rewardFactor)
|
||||
: 0;
|
||||
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)
|
||||
? baseCommissionValue
|
||||
: multiplier * baseCommissionValue;
|
||||
|
||||
const discountFactorValue = refereeStats?.discountFactor
|
||||
? Number(refereeStats.discountFactor)
|
||||
: 0;
|
||||
const currentBenefitTierValue = benefitTiers.find(
|
||||
(t) =>
|
||||
!isNaN(discountFactorValue) &&
|
||||
!isNaN(t.discountFactor) &&
|
||||
t.discountFactor === discountFactorValue
|
||||
);
|
||||
const nextBenefitTierValue = currentBenefitTierValue
|
||||
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1)
|
||||
: maxBy(benefitTiers, (bt) => bt.tier); // max tier number is lowest tier
|
||||
const epochsValue =
|
||||
!isNaN(currentEpoch) && refereeInfo?.atEpoch
|
||||
? currentEpoch - refereeInfo?.atEpoch
|
||||
: 0;
|
||||
const nextBenefitTierVolumeValue = nextBenefitTierValue
|
||||
? nextBenefitTierValue.minimumVolume - runningVolumeValue
|
||||
: 0;
|
||||
const nextBenefitTierEpochsValue = nextBenefitTierValue
|
||||
? nextBenefitTierValue.epochs - epochsValue
|
||||
: 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(),
|
||||
])}
|
||||
>
|
||||
{baseCommissionValue * 100}%
|
||||
</StatTile>
|
||||
);
|
||||
const stakingMultiplierTile = (
|
||||
<StatTile
|
||||
title={t('Staking multiplier')}
|
||||
description={`(${addDecimalsFormatNumber(
|
||||
stakeAvailable?.toString() || 0,
|
||||
18
|
||||
)} $VEGA staked)`}
|
||||
>
|
||||
{multiplier || t('None')}
|
||||
</StatTile>
|
||||
);
|
||||
const finalCommissionTile = (
|
||||
<StatTile
|
||||
title={t('Final commission rate')}
|
||||
description={
|
||||
!isNaN(multiplier)
|
||||
? `(${baseCommissionValue * 100}% ⨉ ${multiplier} = ${
|
||||
finalCommissionValue * 100
|
||||
}%)`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{finalCommissionValue * 100}%
|
||||
</StatTile>
|
||||
);
|
||||
const numberOfTradersValue = data.referees.length;
|
||||
const numberOfTradersTile = (
|
||||
<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)}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
const totalCommissionValue = data.referees
|
||||
.map((r) => new BigNumber(r.totalRefereeGeneratedRewards))
|
||||
.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={t('(qUSD)')}
|
||||
>
|
||||
{getNumberFormat(0).format(Number(totalCommissionValue))}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
const referrerTiles = (
|
||||
<>
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||
{baseCommissionTile}
|
||||
{stakingMultiplierTile}
|
||||
{finalCommissionTile}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{codeTile}
|
||||
{referrerVolumeTile}
|
||||
{numberOfTradersTile}
|
||||
{totalCommissionTile}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const currentBenefitTierTile = (
|
||||
<StatTile title={t('Current tier')}>
|
||||
{currentBenefitTierValue?.tier || 'None'}
|
||||
</StatTile>
|
||||
);
|
||||
const discountFactorTile = (
|
||||
<StatTile title={t('Discount')}>{discountFactorValue * 100}%</StatTile>
|
||||
);
|
||||
const runningVolumeTile = (
|
||||
<StatTile title={t('Combined volume')}>
|
||||
{compactNumFormat.format(runningVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
const epochsTile = (
|
||||
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
|
||||
);
|
||||
const nextTierVolumeTile = (
|
||||
<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 %s',
|
||||
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
|
||||
)}
|
||||
>
|
||||
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
const refereeTiles = (
|
||||
<>
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||
{currentBenefitTierTile}
|
||||
{discountFactorTile}
|
||||
{codeTile}
|
||||
</div>
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{runningVolumeTile}
|
||||
{nextTierVolumeTile}
|
||||
{epochsTile}
|
||||
{nextTierEpochsTile}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
useLayoutEffect(() => {
|
||||
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
|
||||
setCollapsed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Stats tiles */}
|
||||
<div
|
||||
className={classNames('grid grid-cols-1 grid-rows-1 gap-5 mx-auto', {
|
||||
'md:w-1/2': as === 'referee',
|
||||
'md:w-2/3': as === 'referrer',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'grid grid-cols-1 grid-rows-1 gap-5 mx-auto mb-20'
|
||||
)}
|
||||
className={classNames('grid grid-rows-1 gap-5', {
|
||||
'grid-cols-2': as === 'referrer',
|
||||
'grid-cols-1': as === 'referee',
|
||||
})}
|
||||
>
|
||||
{as === 'referrer' && referrerTiles}
|
||||
{as === 'referee' && refereeTiles}
|
||||
{as === 'referrer' && data?.referees && (
|
||||
<Tile className="py-3 h-full">
|
||||
<div className="absolute top-1/2 left-1/2 translate-x-[-50%] translate-y-[-50%]">
|
||||
<h3 className="mb-1 text-6xl text-center">
|
||||
{data.referees.length}
|
||||
</h3>
|
||||
<p className="text-sm text-center text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
{data.referees.length === 1
|
||||
? 'Trader referred'
|
||||
: 'Total traders referred'}
|
||||
</p>
|
||||
</div>
|
||||
</Tile>
|
||||
)}
|
||||
<CodeTile code={data?.code} as={as} />
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<div
|
||||
className={classNames(
|
||||
collapsed && [
|
||||
'relative max-h-96 overflow-hidden',
|
||||
'after:w-full after:h-20 after:absolute after:bottom-0 after:left-0',
|
||||
'after:bg-gradient-to-t after:from-white after:dark:from-vega-cdark-900 after:to-transparent',
|
||||
]
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className={classNames(
|
||||
'absolute left-1/2 bottom-0 z-10 p-2 translate-x-[-50%]',
|
||||
{
|
||||
hidden: !collapsed,
|
||||
}
|
||||
)}
|
||||
onClick={() => setCollapsed(false)}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={24} />
|
||||
</button>
|
||||
<Table
|
||||
ref={tableRef}
|
||||
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: 'commission',
|
||||
displayName: t(
|
||||
'Commission earned (last %s epochs)',
|
||||
(
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS
|
||||
).toString()
|
||||
),
|
||||
},
|
||||
]}
|
||||
data={sortBy(
|
||||
data.referees.map((r) => ({
|
||||
party: (
|
||||
<span title={r.refereeId}>
|
||||
{truncateMiddle(r.refereeId)}
|
||||
</span>
|
||||
),
|
||||
joined: getDateTimeFormat().format(new Date(r.joinedAt)),
|
||||
volume: Number(r.totalRefereeNotionalTakerVolume),
|
||||
commission: Number(r.totalRefereeGeneratedRewards),
|
||||
})),
|
||||
(r) => r.volume
|
||||
)
|
||||
.map((r) => ({
|
||||
...r,
|
||||
volume: getNumberFormat(0).format(r.volume),
|
||||
commission: getNumberFormat(0).format(r.commission),
|
||||
}))
|
||||
.reverse()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
Loader,
|
||||
TradingAnchorButton,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
@@ -7,99 +6,51 @@ import {
|
||||
import { HowItWorksTable } from './how-it-works-table';
|
||||
import { LandingBanner } from './landing-banner';
|
||||
import { TiersContainer } from './tiers';
|
||||
import { TabLink } from './buttons';
|
||||
import { RainbowTabLink } from './buttons';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Routes } from '../../lib/links';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useReferral } from './hooks/use-referral';
|
||||
import { REFERRAL_DOCS_LINK } from './constants';
|
||||
import classNames from 'classnames';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
import { useEffect } from 'react';
|
||||
import { titlefy } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
const Nav = () => (
|
||||
<div className="flex justify-center border-b border-vega-cdark-500">
|
||||
<TabLink end to={Routes.REFERRALS}>
|
||||
{t('I want a code')}
|
||||
</TabLink>
|
||||
<TabLink to={Routes.REFERRALS_APPLY_CODE}>{t('I have a code')}</TabLink>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Referrals = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const {
|
||||
data: referee,
|
||||
loading: refereeLoading,
|
||||
error: refereeError,
|
||||
} = useReferral({
|
||||
pubKey,
|
||||
role: 'referee',
|
||||
});
|
||||
const {
|
||||
data: referrer,
|
||||
loading: referrerLoading,
|
||||
error: referrerError,
|
||||
} = useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
});
|
||||
|
||||
const error = refereeError || referrerError;
|
||||
const loading = refereeLoading || referrerLoading;
|
||||
const showNav = !loading && !error && !referrer && !referee;
|
||||
|
||||
const { updateTitle } = usePageTitleStore((store) => ({
|
||||
updateTitle: store.updateTitle,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
updateTitle(titlefy([t('Referrals')]));
|
||||
}, [updateTitle]);
|
||||
const { data: referee } = useReferral(pubKey, 'referee');
|
||||
const { data: referrer } = useReferral(pubKey, 'referrer');
|
||||
|
||||
return (
|
||||
<>
|
||||
<LandingBanner />
|
||||
|
||||
{showNav && <Nav />}
|
||||
<div
|
||||
className={classNames({
|
||||
'py-16': showNav,
|
||||
'h-[300px] relative': loading || error,
|
||||
})}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Loader />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
|
||||
<p>{t('Something went wrong')}</p>
|
||||
<span className="text-xs">{error.message}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex justify-center">
|
||||
<RainbowTabLink end to={Routes.REFERRALS}>
|
||||
Your referrals
|
||||
</RainbowTabLink>
|
||||
<RainbowTabLink
|
||||
disabled={Boolean(referee || referrer)}
|
||||
to={Routes.REFERRALS_APPLY_CODE}
|
||||
>
|
||||
Apply a code
|
||||
</RainbowTabLink>
|
||||
</div>
|
||||
<div className="py-16 border-t border-b border-vega-cdark-500">
|
||||
<Outlet />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TiersContainer />
|
||||
|
||||
<div className="mt-10 mb-5 text-center">
|
||||
<h2 className="text-2xl">{t('How it works')}</h2>
|
||||
<h2 className="text-2xl">How it works</h2>
|
||||
</div>
|
||||
<div className="md:w-[60%] mx-auto">
|
||||
<HowItWorksTable />
|
||||
<div className="mt-5">
|
||||
<TradingAnchorButton
|
||||
className="mx-auto w-max"
|
||||
href={REFERRAL_DOCS_LINK}
|
||||
href="https://docs.vega.xyz/"
|
||||
target="_blank"
|
||||
>
|
||||
{t('Read the terms')}{' '}
|
||||
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
Read the terms <VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
</TradingAnchorButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { forwardRef, type HTMLAttributes } from 'react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { BORDER_COLOR, GRADIENT } from './constants';
|
||||
|
||||
type TableColumnDefinition = {
|
||||
@@ -19,108 +19,98 @@ type TableProps = {
|
||||
|
||||
const INNER_BORDER_STYLE = `border-b ${BORDER_COLOR}`;
|
||||
|
||||
export const Table = forwardRef<
|
||||
HTMLTableElement,
|
||||
TableProps & HTMLAttributes<HTMLTableElement>
|
||||
>(
|
||||
(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
noHeader = false,
|
||||
noCollapse = false,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const header = (
|
||||
<thead className={classNames({ 'max-md:hidden': !noCollapse })}>
|
||||
<tr>
|
||||
{columns.map(({ displayName, name, tooltip }) => (
|
||||
<th
|
||||
key={name}
|
||||
col-id={name}
|
||||
className={classNames(
|
||||
'px-5 py-3 text-sm text-vega-clight-100 dark:text-vega-cdark-100 font-normal',
|
||||
INNER_BORDER_STYLE
|
||||
)}
|
||||
>
|
||||
<span className="flex flex-row gap-2 items-center">
|
||||
<span>{displayName}</span>
|
||||
{tooltip ? (
|
||||
<Tooltip description={tooltip}>
|
||||
<button className="text-vega-clight-400 dark:text-vega-cdark-400 no-underline decoration-transparent w-[12px] h-[12px] inline-flex">
|
||||
<VegaIcon size={12} name={VegaIconNames.INFO} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
return (
|
||||
<table
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'border-separate border rounded-md border-spacing-0',
|
||||
BORDER_COLOR,
|
||||
GRADIENT,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{!noHeader && header}
|
||||
<tbody>
|
||||
{data.map((d, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className={classNames(d['className'] as string, {
|
||||
'max-md:flex flex-col w-full': !noCollapse,
|
||||
})}
|
||||
>
|
||||
{columns.map(({ name, displayName, className }, j) => (
|
||||
<td
|
||||
className={classNames(
|
||||
'px-5 py-3 text-base',
|
||||
{
|
||||
'max-md:flex max-md:flex-col max-md:justify-between':
|
||||
!noCollapse,
|
||||
},
|
||||
INNER_BORDER_STYLE,
|
||||
{
|
||||
'border-none': i === data.length - 1 && noCollapse,
|
||||
'md:border-none': i === data.length - 1,
|
||||
'max-md:border-none':
|
||||
i === data.length - 1 && j === columns.length - 1,
|
||||
},
|
||||
className
|
||||
export const Table = ({
|
||||
columns,
|
||||
data,
|
||||
noHeader = false,
|
||||
noCollapse = false,
|
||||
className,
|
||||
...props
|
||||
}: TableProps & HTMLAttributes<HTMLTableElement>) => {
|
||||
const header = (
|
||||
<thead className={classNames({ 'max-md:hidden': !noCollapse })}>
|
||||
<tr>
|
||||
{columns.map(({ displayName, name, tooltip }) => (
|
||||
<th
|
||||
key={name}
|
||||
col-id={name}
|
||||
className={classNames(
|
||||
'px-5 py-3 text-sm text-vega-clight-100 dark:text-vega-cdark-100',
|
||||
INNER_BORDER_STYLE
|
||||
)}
|
||||
>
|
||||
<span className="flex flex-row gap-2 items-center">
|
||||
<span>{displayName}</span>
|
||||
{tooltip ? (
|
||||
<Tooltip description={tooltip}>
|
||||
<button className="text-vega-clight-400 dark:text-vega-cdark-400 no-underline decoration-transparent w-[12px] h-[12px] inline-flex">
|
||||
<VegaIcon size={12} name={VegaIconNames.INFO} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
return (
|
||||
<table
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'border-separate border rounded-md border-spacing-0',
|
||||
BORDER_COLOR,
|
||||
GRADIENT,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{!noHeader && header}
|
||||
<tbody>
|
||||
{data.map((d, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className={classNames(d['className'] as string, {
|
||||
'max-md:flex flex-col w-full': !noCollapse,
|
||||
})}
|
||||
>
|
||||
{columns.map(({ name, displayName, className }, j) => (
|
||||
<td
|
||||
className={classNames(
|
||||
'px-5 py-3 text-base',
|
||||
{
|
||||
'max-md:flex max-md:flex-col max-md:justify-between':
|
||||
!noCollapse,
|
||||
},
|
||||
INNER_BORDER_STYLE,
|
||||
{
|
||||
'border-none': i === data.length - 1 && noCollapse,
|
||||
'md:border-none': i === data.length - 1,
|
||||
'max-md:border-none':
|
||||
i === data.length - 1 && j === columns.length - 1,
|
||||
},
|
||||
className
|
||||
)}
|
||||
key={`${i}-${name}`}
|
||||
>
|
||||
{/** display column name in mobile view */}
|
||||
{!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>
|
||||
)}
|
||||
key={`${i}-${name}`}
|
||||
>
|
||||
{/** display column name in mobile view */}
|
||||
{!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>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
);
|
||||
Table.displayName = 'Table';
|
||||
<span>{d[name]}</span>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ export const Tag = ({
|
||||
}: TagProps & HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={classNames(
|
||||
'w-max border rounded-[1rem] py-[0.125rem] px-2 text-xs',
|
||||
'mt-3 w-max border rounded-[1rem] py-[0.125rem] px-2 text-xs',
|
||||
{
|
||||
'border-vega-yellow-500 text-vega-yellow-500': color === 'yellow',
|
||||
'border-vega-green-500 text-vega-green-500': color === 'green',
|
||||
|
||||
@@ -4,10 +4,7 @@ import { Table } from './table';
|
||||
import classNames from 'classnames';
|
||||
import { BORDER_COLOR, GRADIENT } from './constants';
|
||||
import { Tag } from './tag';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { DApp, TOKEN_PROPOSALS, useLinks } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
const Loading = ({ variant }: { variant: 'large' | 'inline' }) => (
|
||||
<div
|
||||
@@ -41,66 +38,51 @@ const StakingTier = ({
|
||||
className={classNames(
|
||||
'overflow-hidden',
|
||||
'border rounded-md w-full',
|
||||
'flex flex-row',
|
||||
GRADIENT,
|
||||
BORDER_COLOR
|
||||
)}
|
||||
>
|
||||
<div aria-hidden className="max-w-[120px]">
|
||||
<div aria-hidden>
|
||||
{tier < 4 && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={`/${tier}x.png`}
|
||||
alt={`${referralRewardMultiplier}x multiplier`}
|
||||
width={240}
|
||||
height={240}
|
||||
className="w-full h-full"
|
||||
width={768}
|
||||
height={400}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={classNames('p-3')}>
|
||||
<Tag color={color[tier]}>Multiplier {referralRewardMultiplier}x</Tag>
|
||||
<h3 className="mt-1 mb-1 text-base">{label}</h3>
|
||||
<p className="text-sm text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
{t('Stake a minimum of')} {minimumStakedTokens} {t('$VEGA tokens')}
|
||||
<div className={classNames('p-3', GRADIENT)}>
|
||||
<h3 className="mb-3 text-xl">{label}</h3>
|
||||
<p className="text-base text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
Stake a minimum of {minimumStakedTokens} $VEGA tokens
|
||||
</p>
|
||||
<Tag color={color[tier]}>
|
||||
Reward multiplier {referralRewardMultiplier}x
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TiersContainer = () => {
|
||||
const { benefitTiers, stakingTiers, details, loading, error } =
|
||||
useReferralProgram();
|
||||
const { benefitTiers, stakingTiers, details, loading } = useReferralProgram();
|
||||
|
||||
const ends = details?.endOfProgramTimestamp
|
||||
? getDateTimeFormat().format(new Date(details.endOfProgramTimestamp))
|
||||
: undefined;
|
||||
|
||||
const governanceLink = useLinks(DApp.Governance);
|
||||
|
||||
if ((!loading && !details) || error) {
|
||||
return (
|
||||
<div className="text-base px-5 py-10 text-center">
|
||||
{t(
|
||||
"We're sorry but we don't have an active referral programme currently running. You can propose a new programme"
|
||||
)}{' '}
|
||||
<ExternalLink href={governanceLink(TOKEN_PROPOSALS)}>
|
||||
{t('here')}
|
||||
</ExternalLink>
|
||||
.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Benefit tiers */}
|
||||
<div className="flex flex-col items-baseline justify-between mt-10 mb-5">
|
||||
<h2 className="text-2xl">{t('Referral tiers')}</h2>
|
||||
<div className="flex flex-row items-baseline justify-between mt-10 mb-5">
|
||||
<h2 className="text-2xl">Referral tiers</h2>
|
||||
{ends && (
|
||||
<span className="text-sm text-vega-clight-200 dark:text-vega-cdark-200">
|
||||
{t('Program ends:')} {ends}
|
||||
<span className="text-base">
|
||||
<span className="text-vega-clight-200 dark:text-vega-cdark-200">
|
||||
Program ends:
|
||||
</span>{' '}
|
||||
{ends}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -108,25 +90,14 @@ export const TiersContainer = () => {
|
||||
{loading || !benefitTiers || benefitTiers.length === 0 ? (
|
||||
<Loading variant="large" />
|
||||
) : (
|
||||
<TiersTable
|
||||
windowLength={details?.windowLength}
|
||||
data={benefitTiers.map((bt) => ({
|
||||
...bt,
|
||||
tierElement: (
|
||||
<div className="rounded-full bg-vega-clight-900 dark:bg-vega-cdark-900 p-1 w-8 h-8 text-center">
|
||||
{bt.tier}
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
<TiersTable data={benefitTiers} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Staking tiers */}
|
||||
<div className="flex flex-row items-baseline justify-between mb-5">
|
||||
<h2 className="text-2xl">{t('Staking multipliers')}</h2>
|
||||
<h2 className="text-2xl">Staking multipliers</h2>
|
||||
</div>
|
||||
<div className="mb-20 flex flex-col justify-items-stretch lg:flex-row gap-5">
|
||||
<div className="flex flex-col mb-20 justify-items-stretch md:flex-row gap-5">
|
||||
{loading || !stakingTiers || stakingTiers.length === 0 ? (
|
||||
<>
|
||||
<Loading variant="large" />
|
||||
@@ -163,37 +134,25 @@ const StakingTiers = ({
|
||||
|
||||
const TiersTable = ({
|
||||
data,
|
||||
windowLength,
|
||||
}: {
|
||||
data: Array<{
|
||||
tier: number;
|
||||
tierElement: ReactNode;
|
||||
commission: string;
|
||||
discount: string;
|
||||
volume: string;
|
||||
}>;
|
||||
windowLength?: number;
|
||||
}) => {
|
||||
return (
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'tierElement', displayName: t('Tier') },
|
||||
{ name: 'tier', displayName: 'Tier' },
|
||||
{
|
||||
name: 'commission',
|
||||
displayName: t('Referrer commission'),
|
||||
tooltip: t('A percentage of commission earned by the referrer'),
|
||||
displayName: 'Referrer commission',
|
||||
tooltip: '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: 'epochs', displayName: t('Min. epochs') },
|
||||
{ name: 'discount', displayName: 'Referrer trading discount' },
|
||||
{ name: 'volume', displayName: 'Min. trading volume' },
|
||||
]}
|
||||
data={data.map((d) => ({
|
||||
...d,
|
||||
|
||||
@@ -1,98 +1,39 @@
|
||||
import {
|
||||
CopyWithTooltip,
|
||||
Tooltip,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { Button } from './buttons';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { BORDER_COLOR, GRADIENT } from './constants';
|
||||
|
||||
type TileProps = {
|
||||
variant?: 'rainbow' | 'default';
|
||||
};
|
||||
|
||||
export const Tile = ({
|
||||
variant = 'default',
|
||||
className,
|
||||
children,
|
||||
}: HTMLAttributes<HTMLDivElement>) => {
|
||||
}: TileProps & HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg overflow-hidden relative',
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white',
|
||||
'p-6',
|
||||
className
|
||||
{
|
||||
'bg-rainbow p-[0.125rem]': variant === 'rainbow',
|
||||
[`border-2 ${BORDER_COLOR} p-0`]: variant === 'default',
|
||||
},
|
||||
'rounded-lg overflow-hidden relative'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<div
|
||||
className={classNames(
|
||||
{
|
||||
'bg-white dark:bg-vega-cdark-900 text-black dark:text-white rounded-[0.35rem] overflow-hidden':
|
||||
variant === 'rainbow',
|
||||
},
|
||||
'p-6',
|
||||
GRADIENT,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type StatTileProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
export const StatTile = ({ title, description, children }: StatTileProps) => {
|
||||
return (
|
||||
<Tile>
|
||||
<h3 className="mb-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100 calt">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="text-5xl text-left">{children}</div>
|
||||
{description && (
|
||||
<div className="text-sm text-left text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</Tile>
|
||||
);
|
||||
};
|
||||
|
||||
const FADE_OUT_STYLE = classNames(
|
||||
'after:w-5 after:h-full after:absolute after:top-0 after:right-0',
|
||||
'after:bg-gradient-to-l after:from-vega-clight-800 after:dark:from-vega-cdark-800 after:to-transparent'
|
||||
);
|
||||
|
||||
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 gap-2 items-center justify-between">
|
||||
<Tooltip
|
||||
description={
|
||||
<div className="break-all">
|
||||
<span className="text-xl bg-rainbow bg-clip-text text-transparent">
|
||||
{code}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'relative bg-rainbow bg-clip-text text-transparent text-5xl overflow-hidden',
|
||||
FADE_OUT_STYLE
|
||||
)}
|
||||
>
|
||||
{code}
|
||||
</div>
|
||||
</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>
|
||||
<VegaIcon size={24} name={VegaIconNames.COPY} />
|
||||
</Button>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
AssetDetailsDialog,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { useUpdateProposal } from '@vegaprotocol/proposals';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
export const LocalAssetDetailsDialog = () => {
|
||||
const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore();
|
||||
const { data: proposal } = useUpdateProposal({
|
||||
id,
|
||||
proposalType: Schema.ProposalType.TYPE_UPDATE_ASSET,
|
||||
});
|
||||
return (
|
||||
<AssetDetailsDialog
|
||||
assetId={id}
|
||||
trigger={trigger || null}
|
||||
open={isOpen}
|
||||
onChange={setOpen}
|
||||
proposalId={proposal?.id ?? undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './asset-details-dialog';
|
||||
@@ -1,58 +0,0 @@
|
||||
query DiscountPrograms {
|
||||
currentReferralProgram {
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
}
|
||||
windowLength
|
||||
}
|
||||
currentVolumeDiscountProgram {
|
||||
benefitTiers {
|
||||
minimumRunningNotionalTakerVolume
|
||||
volumeDiscountFactor
|
||||
}
|
||||
windowLength
|
||||
}
|
||||
}
|
||||
|
||||
query Fees(
|
||||
$partyId: ID!
|
||||
$volumeDiscountEpochs: Int!
|
||||
$referralDiscountEpochs: Int!
|
||||
) {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(
|
||||
partyId: $partyId
|
||||
pagination: { last: $volumeDiscountEpochs }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
runningVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetReferees(referee: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(
|
||||
partyId: $partyId
|
||||
pagination: { last: $referralDiscountEpochs }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
referralSetRunningNotionalTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type DiscountProgramsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgram?: { __typename?: 'CurrentReferralProgram', windowLength: number, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string }> } | null, currentVolumeDiscountProgram?: { __typename?: 'VolumeDiscountProgram', windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } | null };
|
||||
|
||||
export type FeesQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
volumeDiscountEpochs: Types.Scalars['Int'];
|
||||
referralDiscountEpochs: Types.Scalars['Int'];
|
||||
}>;
|
||||
|
||||
|
||||
export type FeesQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string }, volumeDiscountStats: { __typename?: 'VolumeDiscountStatsConnection', edges: Array<{ __typename?: 'VolumeDiscountStatsEdge', node: { __typename?: 'VolumeDiscountStats', atEpoch: number, discountFactor: string, runningVolume: string } } | null> }, referralSetReferees: { __typename?: 'ReferralSetRefereeConnection', edges: Array<{ __typename?: 'ReferralSetRefereeEdge', node: { __typename?: 'ReferralSetReferee', atEpoch: number } } | null> }, referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, discountFactor: string, referralSetRunningNotionalTakerVolume: string } } | null> } };
|
||||
|
||||
|
||||
export const DiscountProgramsDocument = gql`
|
||||
query DiscountPrograms {
|
||||
currentReferralProgram {
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
}
|
||||
windowLength
|
||||
}
|
||||
currentVolumeDiscountProgram {
|
||||
benefitTiers {
|
||||
minimumRunningNotionalTakerVolume
|
||||
volumeDiscountFactor
|
||||
}
|
||||
windowLength
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useDiscountProgramsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useDiscountProgramsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useDiscountProgramsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useDiscountProgramsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDiscountProgramsQuery(baseOptions?: Apollo.QueryHookOptions<DiscountProgramsQuery, DiscountProgramsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<DiscountProgramsQuery, DiscountProgramsQueryVariables>(DiscountProgramsDocument, options);
|
||||
}
|
||||
export function useDiscountProgramsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<DiscountProgramsQuery, DiscountProgramsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<DiscountProgramsQuery, DiscountProgramsQueryVariables>(DiscountProgramsDocument, options);
|
||||
}
|
||||
export type DiscountProgramsQueryHookResult = ReturnType<typeof useDiscountProgramsQuery>;
|
||||
export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>;
|
||||
export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>;
|
||||
export const FeesDocument = gql`
|
||||
query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(
|
||||
partyId: $partyId
|
||||
pagination: {last: $volumeDiscountEpochs}
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
runningVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetReferees(referee: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
referralSetRunningNotionalTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useFeesQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useFeesQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useFeesQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useFeesQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* volumeDiscountEpochs: // value for 'volumeDiscountEpochs'
|
||||
* referralDiscountEpochs: // value for 'referralDiscountEpochs'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useFeesQuery(baseOptions: Apollo.QueryHookOptions<FeesQuery, FeesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<FeesQuery, FeesQueryVariables>(FeesDocument, options);
|
||||
}
|
||||
export function useFeesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FeesQuery, FeesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<FeesQuery, FeesQueryVariables>(FeesDocument, options);
|
||||
}
|
||||
export type FeesQueryHookResult = ReturnType<typeof useFeesQuery>;
|
||||
export type FeesLazyQueryHookResult = ReturnType<typeof useFeesLazyQuery>;
|
||||
export type FeesQueryResult = Apollo.QueryResult<FeesQuery, FeesQueryVariables>;
|
||||
@@ -1,36 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const FeeCard = ({
|
||||
children,
|
||||
title,
|
||||
className,
|
||||
loading = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'p-4 bg-vega-clight-800 dark:bg-vega-cdark-800 col-span-full lg:col-auto',
|
||||
'rounded-lg',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<h2 className="mb-3">{title}</h2>
|
||||
{loading ? <FeeCardLoader /> : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FeeCardLoader = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="w-full h-5 bg-vega-clight-600 dark:bg-vega-cdark-600" />
|
||||
<div className="w-3/4 h-6 bg-vega-clight-600 dark:bg-vega-cdark-600" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,103 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { CurrentVolume, TradingFees } from './fees-container';
|
||||
import { formatPercentage, getAdjustedFee } from './utils';
|
||||
|
||||
describe('TradingFees', () => {
|
||||
it('renders correct fee data', () => {
|
||||
const makerFee = 0.01;
|
||||
const infraFee = 0.01;
|
||||
const minLiqFee = 0.1;
|
||||
const maxLiqFee = 0.3;
|
||||
|
||||
const referralDiscount = 0.01;
|
||||
const volumeDiscount = 0.01;
|
||||
|
||||
const makerBigNum = new BigNumber(makerFee);
|
||||
const infraBigNum = new BigNumber(infraFee);
|
||||
const minLiqBigNum = new BigNumber(minLiqFee);
|
||||
const maxLiqBigNum = new BigNumber(maxLiqFee);
|
||||
const referralBigNum = new BigNumber(referralDiscount);
|
||||
const volumeBigNum = new BigNumber(volumeDiscount);
|
||||
|
||||
const props = {
|
||||
params: {
|
||||
market_fee_factors_makerFee: makerFee.toString(),
|
||||
market_fee_factors_infrastructureFee: infraFee.toString(),
|
||||
},
|
||||
markets: [
|
||||
{ fees: { factors: { liquidityFee: minLiqFee.toString() } } },
|
||||
{ fees: { factors: { liquidityFee: '0.2' } } },
|
||||
{ fees: { factors: { liquidityFee: maxLiqFee.toString() } } },
|
||||
],
|
||||
referralDiscount,
|
||||
volumeDiscount,
|
||||
};
|
||||
render(<TradingFees {...props} />);
|
||||
|
||||
const minFee = formatPercentage(
|
||||
makerBigNum.plus(infraFee).plus(minLiqFee).toNumber()
|
||||
);
|
||||
const maxFee = formatPercentage(
|
||||
makerBigNum.plus(infraFee).plus(maxLiqFee).toNumber()
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText('Total fee before discount').nextElementSibling
|
||||
).toHaveTextContent(`${minFee}%-${maxFee}%`);
|
||||
expect(
|
||||
screen.getByText('Infrastructure').nextElementSibling
|
||||
).toHaveTextContent(formatPercentage(infraFee) + '%');
|
||||
expect(screen.getByText('Maker').nextElementSibling).toHaveTextContent(
|
||||
formatPercentage(makerFee) + '%'
|
||||
);
|
||||
|
||||
const minAdjustedFees = formatPercentage(
|
||||
getAdjustedFee(
|
||||
[makerBigNum, infraBigNum, minLiqBigNum],
|
||||
[referralBigNum, volumeBigNum]
|
||||
)
|
||||
);
|
||||
|
||||
const maxAdjustedFees = formatPercentage(
|
||||
getAdjustedFee(
|
||||
[makerBigNum, infraBigNum, maxLiqBigNum],
|
||||
[referralBigNum, volumeBigNum]
|
||||
)
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('adjusted-fees')).toHaveTextContent(
|
||||
`${minAdjustedFees}%-${maxAdjustedFees}%`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CurerntVolume', () => {
|
||||
it('renders the required amount for the next tier', () => {
|
||||
const windowLengthVolume = 1500;
|
||||
const nextTierVolume = 2000;
|
||||
|
||||
const props = {
|
||||
tiers: [
|
||||
{ minimumRunningNotionalTakerVolume: '1000' },
|
||||
{ minimumRunningNotionalTakerVolume: nextTierVolume.toString() },
|
||||
{ minimumRunningNotionalTakerVolume: '3000' },
|
||||
],
|
||||
tierIndex: 0,
|
||||
windowLengthVolume,
|
||||
epochs: 5,
|
||||
};
|
||||
|
||||
render(<CurrentVolume {...props} />);
|
||||
|
||||
expect(
|
||||
screen.getByText(formatNumber(windowLengthVolume)).nextElementSibling
|
||||
).toHaveTextContent(`Past ${props.epochs} epochs`);
|
||||
|
||||
expect(
|
||||
screen.getByText(formatNumber(nextTierVolume - windowLengthVolume))
|
||||
.nextElementSibling
|
||||
).toHaveTextContent('Required for next tier');
|
||||
});
|
||||
});
|
||||
@@ -1,490 +0,0 @@
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import minBy from 'lodash/minBy';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
useNetworkParams,
|
||||
NetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import { formatNumber, formatNumberRounded } from '@vegaprotocol/utils';
|
||||
import { useDiscountProgramsQuery, useFeesQuery } from './__generated__/Fees';
|
||||
import { FeeCard } from './fees-card';
|
||||
import { MarketFees } from './market-fees';
|
||||
import { Stat } from './stat';
|
||||
import { useVolumeStats } from './use-volume-stats';
|
||||
import { useReferralStats } from './use-referral-stats';
|
||||
import { formatPercentage, getAdjustedFee } from './utils';
|
||||
import { Table, Td, Th, THead, Tr } from './table';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export const FeesContainer = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { params, loading: paramsLoading } = useNetworkParams([
|
||||
NetworkParams.market_fee_factors_makerFee,
|
||||
NetworkParams.market_fee_factors_infrastructureFee,
|
||||
]);
|
||||
|
||||
const { data: markets, loading: marketsLoading } = useMarketList();
|
||||
|
||||
const { data: programData, loading: programLoading } =
|
||||
useDiscountProgramsQuery();
|
||||
|
||||
const volumeDiscountEpochs =
|
||||
programData?.currentVolumeDiscountProgram?.windowLength || 1;
|
||||
const referralDiscountEpochs =
|
||||
programData?.currentReferralProgram?.windowLength || 1;
|
||||
|
||||
const { data: feesData, loading: feesLoading } = useFeesQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
volumeDiscountEpochs,
|
||||
referralDiscountEpochs,
|
||||
},
|
||||
skip: !pubKey || !programData,
|
||||
});
|
||||
|
||||
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
|
||||
useVolumeStats(
|
||||
feesData?.volumeDiscountStats,
|
||||
programData?.currentVolumeDiscountProgram
|
||||
);
|
||||
|
||||
const {
|
||||
referralDiscount,
|
||||
referralVolumeInWindow,
|
||||
referralTierIndex,
|
||||
referralTiers,
|
||||
epochsInSet,
|
||||
} = useReferralStats(
|
||||
feesData?.referralSetStats,
|
||||
feesData?.referralSetReferees,
|
||||
programData?.currentReferralProgram,
|
||||
feesData?.epoch
|
||||
);
|
||||
|
||||
const loading = paramsLoading || feesLoading || programLoading;
|
||||
const isConnected = Boolean(pubKey);
|
||||
|
||||
return (
|
||||
<div className="grid auto-rows-min grid-cols-4 gap-3">
|
||||
{isConnected && (
|
||||
<>
|
||||
<FeeCard
|
||||
title={t('My trading fees')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<TradingFees
|
||||
params={params}
|
||||
markets={markets}
|
||||
referralDiscount={referralDiscount}
|
||||
volumeDiscount={volumeDiscount}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
title={t('Total discount')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<TotalDiscount
|
||||
referralDiscount={referralDiscount}
|
||||
volumeDiscount={volumeDiscount}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
title={t('My current volume')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<CurrentVolume
|
||||
tiers={volumeTiers}
|
||||
tierIndex={volumeTierIndex}
|
||||
windowLengthVolume={volumeInWindow}
|
||||
epochs={volumeDiscountEpochs}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
title={t('Referral benefits')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<ReferralBenefits
|
||||
setRunningNotionalTakerVolume={referralVolumeInWindow}
|
||||
epochsInSet={epochsInSet}
|
||||
epochs={referralDiscountEpochs}
|
||||
/>
|
||||
</FeeCard>
|
||||
</>
|
||||
)}
|
||||
<FeeCard
|
||||
title={t('Volume discount')}
|
||||
className="lg:col-span-full xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<VolumeTiers
|
||||
tiers={volumeTiers}
|
||||
tierIndex={volumeTierIndex}
|
||||
lastEpochVolume={volumeInWindow}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
title={t('Referral discount')}
|
||||
className="lg:col-span-full xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<ReferralTiers
|
||||
tiers={referralTiers}
|
||||
tierIndex={referralTierIndex}
|
||||
epochsInSet={epochsInSet}
|
||||
referralVolumeInWindow={referralVolumeInWindow}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
title={t('Liquidity fees')}
|
||||
className="lg:col-span-full"
|
||||
loading={marketsLoading}
|
||||
>
|
||||
<MarketFees
|
||||
markets={markets}
|
||||
referralDiscount={referralDiscount}
|
||||
volumeDiscount={volumeDiscount}
|
||||
/>
|
||||
</FeeCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TradingFees = ({
|
||||
params,
|
||||
markets,
|
||||
referralDiscount,
|
||||
volumeDiscount,
|
||||
}: {
|
||||
params: {
|
||||
market_fee_factors_infrastructureFee: string;
|
||||
market_fee_factors_makerFee: string;
|
||||
};
|
||||
markets: Array<{ fees: { factors: { liquidityFee: string } } }> | null;
|
||||
referralDiscount: number;
|
||||
volumeDiscount: number;
|
||||
}) => {
|
||||
const referralDiscountBigNum = new BigNumber(referralDiscount);
|
||||
const volumeDiscountBigNum = new BigNumber(volumeDiscount);
|
||||
|
||||
// Show min and max liquidity fees from all markets
|
||||
const minLiq = minBy(markets, (m) => Number(m.fees.factors.liquidityFee));
|
||||
const maxLiq = maxBy(markets, (m) => Number(m.fees.factors.liquidityFee));
|
||||
|
||||
const total = new BigNumber(params.market_fee_factors_makerFee).plus(
|
||||
new BigNumber(params.market_fee_factors_infrastructureFee)
|
||||
);
|
||||
|
||||
const adjustedTotal = getAdjustedFee(
|
||||
[total],
|
||||
[referralDiscountBigNum, volumeDiscountBigNum]
|
||||
);
|
||||
|
||||
let minTotal;
|
||||
let maxTotal;
|
||||
|
||||
let minAdjustedTotal;
|
||||
let maxAdjustedTotal;
|
||||
|
||||
if (minLiq && maxLiq) {
|
||||
const minLiqFee = new BigNumber(minLiq.fees.factors.liquidityFee);
|
||||
const maxLiqFee = new BigNumber(maxLiq.fees.factors.liquidityFee);
|
||||
|
||||
minTotal = total.plus(minLiqFee);
|
||||
maxTotal = total.plus(maxLiqFee);
|
||||
|
||||
minAdjustedTotal = getAdjustedFee(
|
||||
[total, minLiqFee],
|
||||
[referralDiscountBigNum, volumeDiscountBigNum]
|
||||
);
|
||||
|
||||
maxAdjustedTotal = getAdjustedFee(
|
||||
[total, maxLiqFee],
|
||||
[referralDiscountBigNum, volumeDiscountBigNum]
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="pt-6 leading-none">
|
||||
<p className="block text-3xl leading-none" data-testid="adjusted-fees">
|
||||
{minAdjustedTotal !== undefined && maxAdjustedTotal !== undefined
|
||||
? `${formatPercentage(minAdjustedTotal)}%-${formatPercentage(
|
||||
maxAdjustedTotal
|
||||
)}%`
|
||||
: `${formatPercentage(adjustedTotal)}%`}
|
||||
</p>
|
||||
<table className="w-full mt-0.5 text-xs text-muted">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className="font-normal text-left text-default">
|
||||
{t('Total fee before discount')}
|
||||
</th>
|
||||
<td className="text-right text-default">
|
||||
{minTotal !== undefined && maxTotal !== undefined
|
||||
? `${formatPercentage(
|
||||
minTotal.toNumber()
|
||||
)}%-${formatPercentage(maxTotal.toNumber())}%`
|
||||
: `${formatPercentage(total.toNumber())}%`}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="font-normal text-left">{t('Infrastructure')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(
|
||||
Number(params.market_fee_factors_infrastructureFee)
|
||||
)}
|
||||
%
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="font-normal text-left ">{t('Maker')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(Number(params.market_fee_factors_makerFee))}%
|
||||
</td>
|
||||
</tr>
|
||||
{minLiq && maxLiq && (
|
||||
<tr>
|
||||
<th className="font-normal text-left ">{t('Liquidity')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(Number(minLiq.fees.factors.liquidityFee))}%
|
||||
{'-'}
|
||||
{formatPercentage(Number(maxLiq.fees.factors.liquidityFee))}%
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CurrentVolume = ({
|
||||
tiers,
|
||||
tierIndex,
|
||||
windowLengthVolume,
|
||||
epochs,
|
||||
}: {
|
||||
tiers: Array<{ minimumRunningNotionalTakerVolume: string }>;
|
||||
tierIndex: number;
|
||||
windowLengthVolume: number;
|
||||
epochs: number;
|
||||
}) => {
|
||||
const nextTier = tiers[tierIndex + 1];
|
||||
const requiredForNextTier = nextTier
|
||||
? Number(nextTier.minimumRunningNotionalTakerVolume) - windowLengthVolume
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
|
||||
text={t('Past %s epochs', epochs.toString())}
|
||||
/>
|
||||
{requiredForNextTier > 0 && (
|
||||
<Stat
|
||||
value={formatNumber(requiredForNextTier)}
|
||||
text={t('Required for next tier')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ReferralBenefits = ({
|
||||
epochsInSet,
|
||||
setRunningNotionalTakerVolume,
|
||||
epochs,
|
||||
}: {
|
||||
epochsInSet: number;
|
||||
setRunningNotionalTakerVolume: number;
|
||||
epochs: number;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
// all sets volume (not just current party)
|
||||
value={formatNumber(setRunningNotionalTakerVolume)}
|
||||
text={t(
|
||||
'Combined running notional over the %s epochs',
|
||||
epochs.toString()
|
||||
)}
|
||||
/>
|
||||
<Stat value={epochsInSet} text={t('epochs in referral set')} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TotalDiscount = ({
|
||||
referralDiscount,
|
||||
volumeDiscount,
|
||||
}: {
|
||||
referralDiscount: number;
|
||||
volumeDiscount: number;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
value={formatPercentage(referralDiscount + volumeDiscount) + '%'}
|
||||
highlight={true}
|
||||
/>
|
||||
<table className="w-full mt-0.5 text-xs text-muted">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className="font-normal text-left">{t('Volume discount')}</th>
|
||||
<td className="text-right">{formatPercentage(volumeDiscount)}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="font-normal text-left ">{t('Referral discount')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(referralDiscount)}%
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VolumeTiers = ({
|
||||
tiers,
|
||||
tierIndex,
|
||||
lastEpochVolume,
|
||||
}: {
|
||||
tiers: Array<{
|
||||
volumeDiscountFactor: string;
|
||||
minimumRunningNotionalTakerVolume: string;
|
||||
}>;
|
||||
tierIndex: number;
|
||||
lastEpochVolume: number;
|
||||
}) => {
|
||||
if (!tiers.length) {
|
||||
return (
|
||||
<p className="text-sm text-muted">
|
||||
{t('No volume discount program active')}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<THead>
|
||||
<tr>
|
||||
<Th>{t('Tier')}</Th>
|
||||
<Th>{t('Discount')}</Th>
|
||||
<Th>{t('Min. trading volume')}</Th>
|
||||
<Th>{t('My volume (last epoch)')}</Th>
|
||||
<Th />
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers)
|
||||
.reverse()
|
||||
.map((tier, i) => {
|
||||
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>
|
||||
{formatPercentage(Number(tier.volumeDiscountFactor))}%
|
||||
</Td>
|
||||
<Td>
|
||||
{formatNumber(tier.minimumRunningNotionalTakerVolume)}
|
||||
</Td>
|
||||
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : null}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ReferralTiers = ({
|
||||
tiers,
|
||||
tierIndex,
|
||||
epochsInSet,
|
||||
referralVolumeInWindow,
|
||||
}: {
|
||||
tiers: Array<{
|
||||
referralDiscountFactor: string;
|
||||
minimumRunningNotionalTakerVolume: string;
|
||||
minimumEpochs: number;
|
||||
}>;
|
||||
tierIndex: number;
|
||||
epochsInSet: number;
|
||||
referralVolumeInWindow: number;
|
||||
}) => {
|
||||
if (!tiers.length) {
|
||||
return (
|
||||
<p className="text-sm text-muted">{t('No referral program active')}</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<THead>
|
||||
<tr>
|
||||
<Th>{t('Tier')}</Th>
|
||||
<Th>{t('Discount')}</Th>
|
||||
<Th>{t('Min. trading volume')}</Th>
|
||||
<Th>{t('Required epochs')}</Th>
|
||||
<Th />
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers)
|
||||
.reverse()
|
||||
.map((t, i) => {
|
||||
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||
|
||||
const requiredVolume = Number(
|
||||
t.minimumRunningNotionalTakerVolume
|
||||
);
|
||||
let unlocksIn = null;
|
||||
|
||||
if (
|
||||
referralVolumeInWindow >= requiredVolume &&
|
||||
epochsInSet < t.minimumEpochs
|
||||
) {
|
||||
unlocksIn = (
|
||||
<span className="text-muted">
|
||||
Unlocks in {t.minimumEpochs - epochsInSet} epochs
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
|
||||
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
|
||||
<Td>{t.minimumEpochs}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const YourTier = () => {
|
||||
return (
|
||||
<span className="px-4 py-1.5 rounded-xl bg-rainbow whitespace-nowrap text-white">
|
||||
{t('Your tier')}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { FeesContainer } from './fees-container';
|
||||
@@ -1,104 +0,0 @@
|
||||
import compact from 'lodash/compact';
|
||||
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
|
||||
import { AgGrid } from '@vegaprotocol/datagrid';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { formatPercentage, getAdjustedFee } from './utils';
|
||||
import { MarketCodeCell } from '../../client-pages/markets/market-code-cell';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useNavigateWithMeta } from '../../lib/hooks/use-market-click-handler';
|
||||
import { Links } from '../../lib/links';
|
||||
|
||||
const feesTableColumnDefs = [
|
||||
{ field: 'code', cellRenderer: 'MarketCodeCell' },
|
||||
{
|
||||
field: 'feeAfterDiscount',
|
||||
headerName: t('Total fee after discount'),
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'infraFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'makerFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'liquidityFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'totalFee',
|
||||
headerName: t('Total fee before discount'),
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
];
|
||||
|
||||
const feesTableDefaultColDef = {
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
};
|
||||
|
||||
const components = {
|
||||
MarketCodeCell,
|
||||
};
|
||||
|
||||
export const MarketFees = ({
|
||||
markets,
|
||||
referralDiscount,
|
||||
volumeDiscount,
|
||||
}: {
|
||||
markets: MarketMaybeWithDataAndCandles[] | null;
|
||||
referralDiscount: number;
|
||||
volumeDiscount: number;
|
||||
}) => {
|
||||
const navigateWithMeta = useNavigateWithMeta();
|
||||
|
||||
const rows = compact(markets || []).map((m) => {
|
||||
const infraFee = new BigNumber(m.fees.factors.infrastructureFee);
|
||||
const makerFee = new BigNumber(m.fees.factors.makerFee);
|
||||
const liquidityFee = new BigNumber(m.fees.factors.liquidityFee);
|
||||
const totalFee = infraFee.plus(makerFee).plus(liquidityFee);
|
||||
|
||||
const feeAfterDiscount = getAdjustedFee(
|
||||
[infraFee, makerFee, liquidityFee],
|
||||
[new BigNumber(referralDiscount), new BigNumber(volumeDiscount)]
|
||||
);
|
||||
|
||||
return {
|
||||
id: m.id,
|
||||
code: m.tradableInstrument.instrument.code,
|
||||
productType: m.tradableInstrument.instrument.product.__typename,
|
||||
infraFee: formatPercentage(infraFee.toNumber()),
|
||||
makerFee: formatPercentage(makerFee.toNumber()),
|
||||
liquidityFee: formatPercentage(liquidityFee.toNumber()),
|
||||
totalFee: formatPercentage(totalFee.toNumber()),
|
||||
feeAfterDiscount: formatPercentage(feeAfterDiscount),
|
||||
parentMarketID: m.parentMarketID,
|
||||
successorMarketID: m.successorMarketID,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="border rounded-sm border-default">
|
||||
<AgGrid
|
||||
columnDefs={feesTableColumnDefs}
|
||||
rowData={rows}
|
||||
getRowId={({ data }) => data.id}
|
||||
defaultColDef={feesTableDefaultColDef}
|
||||
domLayout="autoHeight"
|
||||
components={components}
|
||||
rowHeight={45}
|
||||
rowClass="cursor-pointer"
|
||||
onRowClicked={({ data, event }) => {
|
||||
navigateWithMeta(
|
||||
Links.MARKET(data.id),
|
||||
// @ts-ignore metaKey and ctrlKey exist
|
||||
event.metaKey || event.ctrlKey
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const Stat = ({
|
||||
value,
|
||||
text,
|
||||
highlight,
|
||||
}: {
|
||||
value: string | number;
|
||||
text?: string;
|
||||
highlight?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<p className="pt-3 leading-none first:pt-6">
|
||||
<span
|
||||
className={classNames('inline-block text-3xl leading-none', {
|
||||
'text-transparent bg-rainbow bg-clip-text': highlight,
|
||||
})}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
{text && (
|
||||
<small className="block mt-0.5 text-xs text-muted">{text}</small>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const cellClass = 'px-4 py-2 text-xs font-normal text-left last:text-right';
|
||||
|
||||
export const Th = ({ children }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<th className={classNames(cellClass, 'text-secondary leading-none py-3')}>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
};
|
||||
|
||||
export const Td = ({ children }: { children?: ReactNode }) => {
|
||||
return <th className={cellClass}>{children}</th>;
|
||||
};
|
||||
|
||||
export const Tr = ({ children }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<tr className="hover:bg-vega-clight-600 dark:hover:bg-vega-cdark-700">
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export const Table = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<table className="w-full border border-separate rounded-sm border-spacing-0 border-vega-clight-600 dark:border-vega-cdark-600">
|
||||
{children}
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export const THead = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<thead className="border-b bg-vega-clight-700 dark:bg-vega-cdark-700 border-vega-clight-600 dark:border-vega-cdark-600">
|
||||
{children}
|
||||
</thead>
|
||||
);
|
||||
};
|
||||
@@ -1,175 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useReferralStats } from './use-referral-stats';
|
||||
|
||||
describe('useReferralStats', () => {
|
||||
const setStats = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.2',
|
||||
referralSetRunningNotionalTakerVolume: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: '200',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const sets = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
atEpoch: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const epoch = {
|
||||
id: '10',
|
||||
};
|
||||
|
||||
const program = {
|
||||
windowLength: 5,
|
||||
benefitTiers: [
|
||||
{
|
||||
minimumEpochs: 4,
|
||||
minimumRunningNotionalTakerVolume: '100',
|
||||
referralDiscountFactor: '0.01',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 6,
|
||||
minimumRunningNotionalTakerVolume: '200',
|
||||
referralDiscountFactor: '0.05',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 8,
|
||||
minimumRunningNotionalTakerVolume: '300',
|
||||
referralDiscountFactor: '0.1',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('returns correct default values', () => {
|
||||
const { result } = renderHook(() => useReferralStats());
|
||||
expect(result.current).toEqual({
|
||||
referralDiscount: 0,
|
||||
referralVolumeInWindow: 0,
|
||||
referralTierIndex: -1,
|
||||
referralTiers: [],
|
||||
epochsInSet: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(setStats, sets, program, epoch)
|
||||
);
|
||||
|
||||
// should use stats from latest epoch
|
||||
const stats = setStats.edges[1].node;
|
||||
const set = sets.edges[1].node;
|
||||
|
||||
expect(result.current).toEqual({
|
||||
referralDiscount: Number(stats.discountFactor),
|
||||
referralVolumeInWindow: Number(
|
||||
stats.referralSetRunningNotionalTakerVolume
|
||||
),
|
||||
referralTierIndex: 1,
|
||||
referralTiers: program.benefitTiers,
|
||||
epochsInSet: Number(epoch.id) - set.atEpoch,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ joinedAt: 2, index: -1 },
|
||||
{ joinedAt: 3, index: -1 },
|
||||
{ joinedAt: 4, index: 0 },
|
||||
{ joinedAt: 5, index: 0 },
|
||||
{ joinedAt: 6, index: 1 },
|
||||
{ joinedAt: 7, index: 1 },
|
||||
{ joinedAt: 8, index: 2 },
|
||||
{ joinedAt: 9, index: 2 },
|
||||
])('joined at epoch: $joinedAt should be index: $index', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: '100000',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const setsA = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: Number(epoch.id) - obj.joinedAt,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(statsA, setsA, program, epoch)
|
||||
);
|
||||
|
||||
expect(result.current.referralTierIndex).toEqual(obj.index);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ volume: '50', index: -1 },
|
||||
{ volume: '100', index: 0 },
|
||||
{ volume: '150', index: 0 },
|
||||
{ volume: '200', index: 1 },
|
||||
{ volume: '250', index: 1 },
|
||||
{ volume: '300', index: 2 },
|
||||
{ volume: '999', index: 2 },
|
||||
])('volume: $volume should be index: $index', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: obj.volume,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const setsA = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(statsA, setsA, program, epoch)
|
||||
);
|
||||
|
||||
expect(result.current.referralTierIndex).toEqual(obj.index);
|
||||
});
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getReferralBenefitTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
|
||||
export const useReferralStats = (
|
||||
setStats?: FeesQuery['referralSetStats'],
|
||||
setReferees?: FeesQuery['referralSetReferees'],
|
||||
program?: DiscountProgramsQuery['currentReferralProgram'],
|
||||
epoch?: FeesQuery['epoch']
|
||||
) => {
|
||||
const referralTiers = program?.benefitTiers || [];
|
||||
|
||||
if (!setStats || !setReferees || !program || !epoch) {
|
||||
return {
|
||||
referralDiscount: 0,
|
||||
referralVolumeInWindow: 0,
|
||||
referralTierIndex: -1,
|
||||
referralTiers,
|
||||
epochsInSet: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const referralSetsStats = compact(setStats.edges).map((e) => e.node);
|
||||
const referralSets = compact(setReferees.edges).map((e) => e.node);
|
||||
|
||||
const referralSet = maxBy(referralSets, (s) => s.atEpoch);
|
||||
const referralStats = maxBy(referralSetsStats, (s) => s.atEpoch);
|
||||
|
||||
const epochsInSet = referralSet ? Number(epoch.id) - referralSet.atEpoch : 0;
|
||||
|
||||
const referralDiscount = Number(referralStats?.discountFactor || 0);
|
||||
const referralVolumeInWindow = Number(
|
||||
referralStats?.referralSetRunningNotionalTakerVolume || 0
|
||||
);
|
||||
|
||||
const referralTierIndex = referralStats
|
||||
? getReferralBenefitTier(
|
||||
epochsInSet,
|
||||
Number(referralStats.referralSetRunningNotionalTakerVolume),
|
||||
referralTiers
|
||||
)
|
||||
: -1;
|
||||
|
||||
return {
|
||||
referralDiscount,
|
||||
referralVolumeInWindow,
|
||||
referralTierIndex,
|
||||
referralTiers,
|
||||
epochsInSet,
|
||||
};
|
||||
};
|
||||
@@ -1,95 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useVolumeStats } from './use-volume-stats';
|
||||
|
||||
describe('useReferralStats', () => {
|
||||
const statsList = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.1',
|
||||
runningVolume: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
runningVolume: '200',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const program = {
|
||||
windowLength: 5,
|
||||
benefitTiers: [
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '100',
|
||||
volumeDiscountFactor: '0.01',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '200',
|
||||
volumeDiscountFactor: '0.05',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '300',
|
||||
volumeDiscountFactor: '0.1',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('returns correct default values', () => {
|
||||
const { result } = renderHook(() => useVolumeStats());
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: 0,
|
||||
volumeInWindow: 0,
|
||||
volumeTierIndex: -1,
|
||||
volumeTiers: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(statsList, program));
|
||||
|
||||
// should use stats from latest epoch
|
||||
const stats = statsList.edges[1].node;
|
||||
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: Number(stats.discountFactor),
|
||||
volumeInWindow: Number(stats.runningVolume),
|
||||
volumeTierIndex: 1,
|
||||
volumeTiers: program.benefitTiers,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ volume: '100', index: 0 },
|
||||
{ volume: '150', index: 0 },
|
||||
{ volume: '200', index: 1 },
|
||||
{ volume: '250', index: 1 },
|
||||
{ volume: '300', index: 2 },
|
||||
{ volume: '350', index: 2 },
|
||||
])('returns index: $index for the running volume: $volume', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
runningVolume: obj.volume,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useVolumeStats(statsA, program));
|
||||
expect(result.current.volumeTierIndex).toBe(obj.index);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getVolumeTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
|
||||
export const useVolumeStats = (
|
||||
stats?: FeesQuery['volumeDiscountStats'],
|
||||
program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
|
||||
) => {
|
||||
const volumeTiers = program?.benefitTiers || [];
|
||||
|
||||
if (!stats || !program) {
|
||||
return {
|
||||
volumeDiscount: 0,
|
||||
volumeTierIndex: -1,
|
||||
volumeInWindow: 0,
|
||||
volumeTiers,
|
||||
};
|
||||
}
|
||||
|
||||
const volumeStats = compact(stats.edges).map((e) => e.node);
|
||||
const lastEpochStats = maxBy(volumeStats, (s) => s.atEpoch);
|
||||
const volumeDiscount = Number(lastEpochStats?.discountFactor || 0);
|
||||
const volumeInWindow = Number(lastEpochStats?.runningVolume || 0);
|
||||
const volumeTierIndex = getVolumeTier(volumeInWindow, volumeTiers);
|
||||
|
||||
return {
|
||||
volumeDiscount,
|
||||
volumeTierIndex,
|
||||
volumeInWindow,
|
||||
volumeTiers,
|
||||
};
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { getAdjustedFee } from './utils';
|
||||
|
||||
describe('getAdjustedFee', () => {
|
||||
it('simple', () => {
|
||||
const volumeDiscount = 0.5;
|
||||
const referralDiscount = 0.5;
|
||||
|
||||
const infraFee = 0.1;
|
||||
const makerFee = 0.1;
|
||||
const liqFee = 0.1;
|
||||
|
||||
const fees = [
|
||||
new BigNumber(infraFee),
|
||||
new BigNumber(makerFee),
|
||||
new BigNumber(liqFee),
|
||||
];
|
||||
|
||||
const discounts = [
|
||||
new BigNumber(volumeDiscount),
|
||||
new BigNumber(referralDiscount),
|
||||
];
|
||||
|
||||
// 1 - 0.5 - 0.5
|
||||
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
|
||||
|
||||
// 1 - 0.5 = 0.5
|
||||
const r = new BigNumber(1).minus(new BigNumber(referralDiscount));
|
||||
|
||||
// 0.5 * 0.5 = 0.25
|
||||
// 1 - 0.25 = 0.75
|
||||
const factor = new BigNumber(1).minus(v.times(r));
|
||||
|
||||
// 0.1 + 0.1 + 0.1 = 0.3
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
// 0.3 * 0.75 = 0.225
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
|
||||
it('combines discount factors multiplicativly', () => {
|
||||
const volumeDiscount = 0.4;
|
||||
const referralDiscount = 0.1;
|
||||
|
||||
const infraFee = 0.0005;
|
||||
const makerFee = 0.0002;
|
||||
const liqFee = 0.01;
|
||||
|
||||
const fees = [
|
||||
new BigNumber(infraFee),
|
||||
new BigNumber(makerFee),
|
||||
new BigNumber(liqFee),
|
||||
];
|
||||
|
||||
const discounts = [
|
||||
new BigNumber(volumeDiscount),
|
||||
new BigNumber(referralDiscount),
|
||||
];
|
||||
|
||||
// formula for calculating adjusted fees
|
||||
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
|
||||
const r = new BigNumber(1).minus(new BigNumber(referralDiscount));
|
||||
const factor = new BigNumber(1).minus(v.times(r));
|
||||
|
||||
// summed fees
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
import { getUserLocale } from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
/**
|
||||
* Convert a number between 0-1 into a percentage value between 0-100
|
||||
*
|
||||
* Not using formatNumberPercentage from vegaprotocol/utils as this
|
||||
* returns a string and includes extra 0s on the end. We need these
|
||||
* values in aggrid as numbers for sorting
|
||||
*/
|
||||
export const formatPercentage = (num: number) => {
|
||||
const pct = new BigNumber(num).times(100);
|
||||
const dps = pct.decimalPlaces();
|
||||
const formatter = new Intl.NumberFormat(getUserLocale(), {
|
||||
minimumFractionDigits: dps || 0,
|
||||
maximumFractionDigits: dps || 0,
|
||||
});
|
||||
return formatter.format(parseFloat(pct.toFixed(5)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the index of the benefit tier for volume discounts. A user
|
||||
* only needs to fulfill a minimum volume requirement for the tier
|
||||
*/
|
||||
export const getVolumeTier = (
|
||||
volume: number,
|
||||
tiers: Array<{
|
||||
minimumRunningNotionalTakerVolume: string;
|
||||
}>
|
||||
) => {
|
||||
return tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validVolume =
|
||||
volume >= Number(tier.minimumRunningNotionalTakerVolume);
|
||||
|
||||
if (nextTier) {
|
||||
return (
|
||||
validVolume &&
|
||||
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
|
||||
);
|
||||
}
|
||||
|
||||
return validVolume;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the index of the benefit tiers for referrals. A user must
|
||||
* fulfill both the minimum epochs in the referral set, and the set
|
||||
* must reach the combined total volume
|
||||
*/
|
||||
export const getReferralBenefitTier = (
|
||||
epochsInSet: number,
|
||||
volume: number,
|
||||
tiers: Array<{
|
||||
minimumRunningNotionalTakerVolume: string;
|
||||
minimumEpochs: number;
|
||||
}>
|
||||
) => {
|
||||
const indexByEpoch = tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validEpochs = epochsInSet >= tier.minimumEpochs;
|
||||
|
||||
if (nextTier) {
|
||||
return validEpochs && epochsInSet < nextTier.minimumEpochs;
|
||||
}
|
||||
|
||||
return validEpochs;
|
||||
});
|
||||
const indexByVolume = tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validVolume =
|
||||
volume >= Number(tier.minimumRunningNotionalTakerVolume);
|
||||
|
||||
if (nextTier) {
|
||||
return (
|
||||
validVolume &&
|
||||
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
|
||||
);
|
||||
}
|
||||
|
||||
return validVolume;
|
||||
});
|
||||
|
||||
return Math.min(indexByEpoch, indexByVolume);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a set of fees and a set of discounts return
|
||||
* the adjusted fee factor
|
||||
*
|
||||
* Formula for calculating the adjusted fees
|
||||
* total_discount_factor = 1 - (1 - volumeDiscount) * (1 - referralDiscount)
|
||||
*/
|
||||
export const getAdjustedFee = (fees: BigNumber[], discounts: BigNumber[]) => {
|
||||
const totalFee = fees.reduce((sum, f) => sum.plus(f), new BigNumber(0));
|
||||
|
||||
const combinedFactors = discounts.reduce((acc, d) => {
|
||||
return acc.times(new BigNumber(1).minus(d));
|
||||
}, new BigNumber(1));
|
||||
|
||||
const totalFactor = new BigNumber(1).minus(combinedFactors);
|
||||
|
||||
return totalFee.times(BigNumber.max(0, totalFactor)).toNumber();
|
||||
};
|
||||
@@ -27,7 +27,7 @@ export const LayoutWithSidebar = ({
|
||||
<div className={gridClasses}>
|
||||
<div className="col-span-full">{header}</div>
|
||||
<main
|
||||
className={classNames('col-start-1 col-end-1 overflow-y-auto', {
|
||||
className={classNames('col-start-1 col-end-1', {
|
||||
'lg:col-end-3': !sidebarOpen,
|
||||
'hidden lg:block lg:col-end-2': sidebarOpen,
|
||||
})}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -67,7 +67,6 @@ describe('Navbar', () => {
|
||||
[`/markets/${marketId}`, 'Trading'],
|
||||
['/portfolio', 'Portfolio'],
|
||||
['/referrals', 'Referrals'],
|
||||
['/fees', 'Fees'],
|
||||
[expect.stringContaining('governance'), 'Governance'],
|
||||
];
|
||||
|
||||
@@ -101,7 +100,6 @@ describe('Navbar', () => {
|
||||
[`/markets/${marketId}`, 'Trading'],
|
||||
['/portfolio', 'Portfolio'],
|
||||
['/referrals', 'Referrals'],
|
||||
['/fees', 'Fees'],
|
||||
[expect.stringContaining('governance'), 'Governance'],
|
||||
];
|
||||
const links = menu.getAllByRole('link');
|
||||
|
||||
@@ -182,16 +182,11 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
||||
</NavbarItem>
|
||||
{FLAGS.REFERRALS && (
|
||||
<NavbarItem>
|
||||
<NavbarLink end={false} to={Links.REFERRALS()} onClick={onClick}>
|
||||
<NavbarLink to={Links.REFERRALS()} onClick={onClick}>
|
||||
{t('Referrals')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
)}
|
||||
<NavbarItem>
|
||||
<NavbarLink to={Links.FEES()} onClick={onClick}>
|
||||
{t('Fees')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<NavbarLinkExternal to={useLinks(DApp.Governance)()}>
|
||||
{t('Governance')}
|
||||
@@ -260,18 +255,16 @@ const NavbarLink = ({
|
||||
children,
|
||||
to,
|
||||
onClick,
|
||||
end = true,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
to: string;
|
||||
onClick?: () => void;
|
||||
end?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<N.Link asChild={true}>
|
||||
<NavLink
|
||||
to={to}
|
||||
end={end}
|
||||
end={true}
|
||||
className={classNames(
|
||||
'block lg:flex lg:h-full flex-col justify-center',
|
||||
'px-6 py-2 lg:p-0 text-lg lg:text-sm',
|
||||
|
||||
@@ -17,7 +17,6 @@ export const Routes = {
|
||||
REFERRALS_APPLY_CODE: '/referrals/apply-code',
|
||||
REFERRALS_CREATE_CODE: '/referrals/create-code',
|
||||
TEAMS: '/teams',
|
||||
FEES: '/fees',
|
||||
} as const;
|
||||
|
||||
type ConsoleLinks = {
|
||||
@@ -41,5 +40,4 @@ export const Links: ConsoleLinks = {
|
||||
REFERRALS_APPLY_CODE: () => Routes.REFERRALS_APPLY_CODE,
|
||||
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
|
||||
TEAMS: () => Routes.TEAMS,
|
||||
FEES: () => Routes.FEES,
|
||||
};
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Assets } from '../client-pages/assets';
|
||||
import { Deposit } from '../client-pages/deposit';
|
||||
import { Withdraw } from '../client-pages/withdraw';
|
||||
import { Transfer } from '../client-pages/transfer';
|
||||
import { Fees } from '../client-pages/fees';
|
||||
import { Routes as AppRoutes } from '../lib/links';
|
||||
import { LayoutWithSky } from '../client-pages/referrals/layout';
|
||||
import { Referrals } from '../client-pages/referrals/referrals';
|
||||
@@ -56,14 +55,10 @@ export const routerConfig: RouteObject[] = compact([
|
||||
FLAGS.REFERRALS
|
||||
? {
|
||||
path: AppRoutes.REFERRALS,
|
||||
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
|
||||
element: <LayoutWithSky />,
|
||||
children: [
|
||||
{
|
||||
element: (
|
||||
<LayoutWithSky>
|
||||
<Referrals />
|
||||
</LayoutWithSky>
|
||||
),
|
||||
element: <Referrals />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -86,16 +81,6 @@ export const routerConfig: RouteObject[] = compact([
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
path: 'fees/*',
|
||||
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Fees />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'markets/*',
|
||||
element: (
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
AssetDetailsDialog,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { VegaConnectDialog, ViewAsDialog } from '@vegaprotocol/wallet';
|
||||
import { Connectors } from '../lib/vega-connectors';
|
||||
import {
|
||||
@@ -10,9 +6,9 @@ import {
|
||||
} from '@vegaprotocol/web3';
|
||||
import { WelcomeDialog } from '../components/welcome-dialog';
|
||||
import { RiskMessage } from '../components/welcome-dialog';
|
||||
import { LocalAssetDetailsDialog as AssetDetailsDialog } from '../components/asset-details-dialog';
|
||||
|
||||
const DialogsContainer = () => {
|
||||
const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore();
|
||||
return (
|
||||
<>
|
||||
<VegaConnectDialog
|
||||
@@ -20,12 +16,7 @@ const DialogsContainer = () => {
|
||||
riskMessage={<RiskMessage />}
|
||||
/>
|
||||
<ViewAsDialog connector={Connectors.view} />
|
||||
<AssetDetailsDialog
|
||||
assetId={id}
|
||||
trigger={trigger || null}
|
||||
open={isOpen}
|
||||
onChange={setOpen}
|
||||
/>
|
||||
<AssetDetailsDialog />
|
||||
<WelcomeDialog />
|
||||
<Web3ConnectUncontrolledDialog />
|
||||
<WithdrawalApprovalDialogContainer />
|
||||
|
||||
@@ -184,10 +184,10 @@ html [data-theme='dark'] {
|
||||
|
||||
/* Light variables */
|
||||
.ag-theme-balham {
|
||||
--ag-background-color: transparent;
|
||||
--ag-background-color: theme(colors.white);
|
||||
--ag-border-color: theme(colors.vega.clight.600);
|
||||
--ag-header-background-color: theme(colors.vega.clight.700);
|
||||
--ag-odd-row-background-color: transparent;
|
||||
--ag-odd-row-background-color: theme(colors.white);
|
||||
--ag-header-column-separator-color: theme(colors.vega.clight.500);
|
||||
--ag-row-border-color: theme(colors.vega.clight.600);
|
||||
--ag-row-hover-color: theme(colors.vega.clight.800);
|
||||
@@ -196,10 +196,10 @@ html [data-theme='dark'] {
|
||||
|
||||
/* Dark variables */
|
||||
.ag-theme-balham-dark {
|
||||
--ag-background-color: transparent;
|
||||
--ag-background-color: theme(colors.vega.cdark.900);
|
||||
--ag-border-color: theme(colors.vega.cdark.600);
|
||||
--ag-header-background-color: theme(colors.vega.cdark.700);
|
||||
--ag-odd-row-background-color: transparent;
|
||||
--ag-odd-row-background-color: theme(colors.vega.cdark.900);
|
||||
--ag-header-column-separator-color: theme(colors.vega.cdark.500);
|
||||
--ag-row-border-color: theme(colors.vega.cdark.600);
|
||||
--ag-row-hover-color: theme(colors.vega.cdark.800);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { useProposalToasts } from '@vegaprotocol/proposals';
|
||||
import { useProposalNotificationToasts } from '@vegaprotocol/proposals';
|
||||
import { useVegaTransactionToasts } from '@vegaprotocol/web3';
|
||||
import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
|
||||
import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
|
||||
@@ -7,7 +7,7 @@ import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
|
||||
import { Links } from '../lib/links';
|
||||
|
||||
export const ToastsManager = () => {
|
||||
useProposalToasts();
|
||||
useProposalNotificationToasts();
|
||||
useVegaTransactionToasts();
|
||||
useEthereumTransactionToasts();
|
||||
useEthereumWithdrawApprovalsToasts();
|
||||
|
||||
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 614 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 572 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 574 KiB |
|
Before Width: | Height: | Size: 419 KiB After Width: | Height: | Size: 947 KiB |
|
Before Width: | Height: | Size: 419 KiB After Width: | Height: | Size: 947 KiB |
@@ -24,8 +24,8 @@ module.exports = {
|
||||
...theme.backgroundImage,
|
||||
rainbow:
|
||||
'linear-gradient(103.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
'rainbow-180':
|
||||
'linear-gradient(283.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
'rainbow-shifted':
|
||||
'linear-gradient(103.47deg, #0075FF 1.68%, #8028FF 47.49%, #FF077F 100%)',
|
||||
highlight:
|
||||
'linear-gradient(170deg, var(--tw-gradient-from), transparent var(--tw-gradient-to-position))',
|
||||
},
|
||||
@@ -38,144 +38,10 @@ module.exports = {
|
||||
'75%': { transform: 'translateX(5px)' },
|
||||
'100%': { transform: 'translateX(0)' },
|
||||
},
|
||||
'spin-rainbow-180': {
|
||||
'0%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(103.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'10%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(121.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'20%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(139.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'30%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(157.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'40%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(175.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'50%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(193.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'60%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(211.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'70%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(229.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'80%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(247.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'90%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(265.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'100%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(283.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
},
|
||||
'spin-rainbow-360': {
|
||||
'0%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(103.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'5%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(121.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'10%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(139.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'15%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(157.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'20%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(175.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'25%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(193.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'30%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(211.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'35%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(229.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'40%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(247.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'45%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(265.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'50%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(283.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'55%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(301.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'60%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(319.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'65%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(337.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'70%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(355.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'75%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(13.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'80%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(31.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'85%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(49.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'90%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(67.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'95%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(85.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'100%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(103.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
...theme.animation,
|
||||
shake: 'shake 200ms linear',
|
||||
'spin-rainbow': 'spin-rainbow-180 500ms linear',
|
||||
'rotate-rainbow': 'spin-rainbow-360 1000ms linear',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@ import BigNumber from 'bignumber.js';
|
||||
import classNames from 'classnames';
|
||||
import { AccountsActionsDropdown } from './accounts-actions-dropdown';
|
||||
|
||||
const colorClass = (percentageUsed: number) => {
|
||||
const colorClass = (percentageUsed: number, neutral = false) => {
|
||||
return classNames('text-right', {
|
||||
'text-vega-orange': percentageUsed >= 75 && percentageUsed < 90,
|
||||
'text-vega-red': percentageUsed >= 90,
|
||||
@@ -210,7 +210,7 @@ export const AccountTable = ({
|
||||
},
|
||||
cellClass: ({ data }) => {
|
||||
const percentageUsed = percentageValue(data?.used, data?.total);
|
||||
return colorClass(percentageUsed);
|
||||
return colorClass(percentageUsed, true);
|
||||
},
|
||||
valueFormatter: ({
|
||||
value,
|
||||
@@ -270,8 +270,7 @@ export const AccountTable = ({
|
||||
onClickDeposit && onClickDeposit(assetId);
|
||||
}}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.DEPOSIT} size={14} />{' '}
|
||||
{t('Deposit')}
|
||||
<VegaIcon name={VegaIconNames.DEPOSIT} /> {t('Deposit')}
|
||||
</TradingButton>
|
||||
</CenteredGridCellWrapper>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { create } from 'zustand';
|
||||
import { AssetDetailsTable } from './asset-details-table';
|
||||
import { AssetProposalNotification } from '@vegaprotocol/proposals';
|
||||
import { AssetProposalNotification } from './asset-proposal-notification';
|
||||
import { useAssetDataProvider } from './asset-data-provider';
|
||||
|
||||
export type AssetDetailsDialogStore = {
|
||||
@@ -47,6 +47,7 @@ export interface AssetDetailsDialogProps {
|
||||
open: boolean;
|
||||
onChange: (open: boolean) => void;
|
||||
asJson?: boolean;
|
||||
proposalId?: string;
|
||||
}
|
||||
|
||||
export const AssetDetailsDialog = ({
|
||||
@@ -55,6 +56,7 @@ export const AssetDetailsDialog = ({
|
||||
open,
|
||||
onChange,
|
||||
asJson = false,
|
||||
proposalId,
|
||||
}: AssetDetailsDialogProps) => {
|
||||
const { data: asset } = useAssetDataProvider(assetId);
|
||||
|
||||
@@ -62,7 +64,7 @@ export const AssetDetailsDialog = ({
|
||||
|
||||
const content = asset ? (
|
||||
<div className="my-2">
|
||||
<AssetProposalNotification assetId={asset.id} />
|
||||
<AssetProposalNotification proposalId={proposalId} />
|
||||
{asJson ? (
|
||||
<div className="pr-8">
|
||||
<SyntaxHighlighter size="smaller" data={asset} />
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { useUpdateProposal } from '../lib';
|
||||
|
||||
type AssetProposalNotificationProps = {
|
||||
assetId?: string;
|
||||
proposalId?: string;
|
||||
};
|
||||
export const AssetProposalNotification = ({
|
||||
assetId,
|
||||
proposalId,
|
||||
}: AssetProposalNotificationProps) => {
|
||||
const tokenLink = useLinks(DApp.Governance);
|
||||
const { data: proposal } = useUpdateProposal({
|
||||
id: assetId,
|
||||
proposalType: Schema.ProposalType.TYPE_UPDATE_ASSET,
|
||||
});
|
||||
|
||||
if (proposal) {
|
||||
if (proposalId) {
|
||||
const proposalLink = tokenLink(
|
||||
TOKEN_PROPOSAL.replace(':id', proposal.id || '')
|
||||
TOKEN_PROPOSAL.replace(':id', proposalId || '')
|
||||
);
|
||||
const message = (
|
||||
<>
|
||||
@@ -1,83 +1,31 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { CandlesMenu } from './candles-menu';
|
||||
import {
|
||||
useCandlesChartSettingsStore,
|
||||
DEFAULT_CHART_SETTINGS,
|
||||
} from './use-candles-chart-settings';
|
||||
import { Overlay, Study, overlayLabels, studyLabels } from 'pennant';
|
||||
|
||||
describe('CandlesMenu', () => {
|
||||
const openDropdown = async () => {
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Indicators',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// clear store each time to avoid conditional testing of defaults
|
||||
useCandlesChartSettingsStore.setState({ overlays: [], studies: [] });
|
||||
});
|
||||
|
||||
it.each(Object.values(Overlay))('can set %s overlay', async (overlay) => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await openDropdown();
|
||||
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
|
||||
await userEvent.click(menu.getByText(overlayLabels[overlay as Overlay]));
|
||||
|
||||
// re-open the dropdown
|
||||
await openDropdown();
|
||||
|
||||
expect(screen.getByText(overlayLabels[overlay as Overlay])).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
});
|
||||
|
||||
it.each(Object.values(Study))('can set %s study', async (study) => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await openDropdown();
|
||||
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
|
||||
await userEvent.click(menu.getByText(studyLabels[study as Study]));
|
||||
|
||||
// re-open the dropdown
|
||||
await openDropdown();
|
||||
|
||||
expect(screen.getByText(studyLabels[study as Study])).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render with the correct default studies and overlays', async () => {
|
||||
useCandlesChartSettingsStore.setState(DEFAULT_CHART_SETTINGS);
|
||||
|
||||
it('should render with the correct default studies', async () => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Indicators',
|
||||
name: 'Studies',
|
||||
})
|
||||
);
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
||||
expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked');
|
||||
expect(screen.getByText('MACD')).toHaveAttribute('data-state', 'checked');
|
||||
});
|
||||
|
||||
expect(menu.getByText(studyLabels.volume)).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
it('should render with the correct default overlays', async () => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Overlays',
|
||||
})
|
||||
);
|
||||
expect(menu.getByText(studyLabels.macd)).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
expect(menu.getByText(overlayLabels.movingAverage)).toHaveAttribute(
|
||||
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
||||
expect(screen.getByText('Moving average')).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
|
||||
@@ -107,7 +107,7 @@ export const CandlesMenu = () => {
|
||||
trigger={
|
||||
<TradingDropdownTrigger className={triggerClasses}>
|
||||
<TradingButton {...triggerButtonProps}>
|
||||
{t('Indicators')}
|
||||
{t('Overlays')}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
@@ -132,6 +132,18 @@ export const CandlesMenu = () => {
|
||||
<TradingDropdownItemIndicator />
|
||||
</TradingDropdownCheckboxItem>
|
||||
))}
|
||||
</TradingDropdownContent>
|
||||
</TradingDropdown>
|
||||
<TradingDropdown
|
||||
trigger={
|
||||
<TradingDropdownTrigger className={triggerClasses}>
|
||||
<TradingButton {...triggerButtonProps}>
|
||||
{t('Studies')}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
>
|
||||
<TradingDropdownContent align={contentAlign}>
|
||||
{Object.values(Study).map((study) => (
|
||||
<TradingDropdownCheckboxItem
|
||||
key={study}
|
||||
|
||||
@@ -24,7 +24,7 @@ const STUDY_ORDER: Study[] = [
|
||||
Study.VOLUME,
|
||||
];
|
||||
|
||||
export const DEFAULT_CHART_SETTINGS = {
|
||||
const DEFAULT_CHART_SETTINGS = {
|
||||
interval: Interval.I15M,
|
||||
type: ChartType.CANDLE,
|
||||
overlays: [Overlay.MOVING_AVERAGE],
|
||||
|
||||
@@ -3,12 +3,7 @@ import throttle from 'lodash/throttle';
|
||||
import isEqualWith from 'lodash/isEqualWith';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import type { OperationVariables } from '@apollo/client';
|
||||
import type {
|
||||
Subscribe,
|
||||
Load,
|
||||
UpdateCallback,
|
||||
PageInfo,
|
||||
} from './generic-data-provider';
|
||||
import type { Subscribe, Load, UpdateCallback } from './generic-data-provider';
|
||||
import { variablesIsEqualCustomizer } from './generic-data-provider';
|
||||
|
||||
export interface useDataProviderParams<
|
||||
@@ -17,23 +12,13 @@ export interface useDataProviderParams<
|
||||
Variables extends OperationVariables | undefined = undefined
|
||||
> {
|
||||
dataProvider: Subscribe<Data, Delta, Variables>;
|
||||
update?: ({
|
||||
delta,
|
||||
data,
|
||||
pageInfo,
|
||||
}: {
|
||||
delta?: Delta;
|
||||
data: Data | null;
|
||||
pageInfo: PageInfo | null;
|
||||
}) => boolean;
|
||||
update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean;
|
||||
insert?: ({
|
||||
insertionData,
|
||||
data,
|
||||
pageInfo,
|
||||
}: {
|
||||
insertionData?: Data | null;
|
||||
data: Data | null;
|
||||
pageInfo: PageInfo | null;
|
||||
}) => boolean;
|
||||
variables: Variables;
|
||||
skipUpdates?: boolean;
|
||||
@@ -45,7 +30,7 @@ export interface useDataProviderParams<
|
||||
* @param dataProvider subscribe function created by makeDataProvider
|
||||
* @param update optional function called on each delta received in subscription, if returns true updated data will be not passed from hook (component handles updates internally)
|
||||
* @param variables optional
|
||||
* @returns state: data, loading, pageInfo, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
|
||||
* @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
|
||||
*/
|
||||
export const useDataProvider = <
|
||||
Data,
|
||||
@@ -63,7 +48,6 @@ export const useDataProvider = <
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(!skip);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [pageInfo, setPageInfo] = useState<PageInfo | null>(null);
|
||||
const flushRef = useRef<(() => void) | undefined>(undefined);
|
||||
const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined);
|
||||
const loadRef = useRef<Load<Data> | undefined>(undefined);
|
||||
@@ -109,7 +93,6 @@ export const useDataProvider = <
|
||||
isInsert,
|
||||
isUpdate,
|
||||
loaded,
|
||||
pageInfo,
|
||||
} = args;
|
||||
setError(error);
|
||||
setLoading(!loaded && loading);
|
||||
@@ -121,22 +104,21 @@ export const useDataProvider = <
|
||||
(skipUpdatesRef.current ||
|
||||
(!skipUpdatesRef.current &&
|
||||
updateRef.current &&
|
||||
updateRef.current({ delta, data, pageInfo })))
|
||||
updateRef.current({ delta, data })))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isInsert &&
|
||||
insertRef.current &&
|
||||
insertRef.current({ insertionData, data, pageInfo })
|
||||
insertRef.current({ insertionData, data })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setData(data);
|
||||
setPageInfo(pageInfo);
|
||||
if (!loading && !isUpdate && updateRef.current) {
|
||||
updateRef.current({ data, pageInfo });
|
||||
updateRef.current({ data });
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -154,13 +136,15 @@ export const useDataProvider = <
|
||||
|
||||
useEffect(() => {
|
||||
setData(null);
|
||||
setPageInfo(null);
|
||||
setError(undefined);
|
||||
if (updateRef.current) {
|
||||
updateRef.current({ data: null, pageInfo: null });
|
||||
updateRef.current({ data: null });
|
||||
}
|
||||
if (skip) {
|
||||
setLoading(false);
|
||||
if (updateRef.current) {
|
||||
updateRef.current({ data: null });
|
||||
}
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -181,7 +165,6 @@ export const useDataProvider = <
|
||||
}, [client, dataProvider, callback, variables, skip]);
|
||||
return {
|
||||
data,
|
||||
pageInfo,
|
||||
loading,
|
||||
error,
|
||||
flush,
|
||||
|
||||
@@ -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,49 +63,48 @@ 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>
|
||||
)}
|
||||
{totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`}
|
||||
{feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(
|
||||
feeEstimate?.totalFeeAmount,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}`}
|
||||
</>
|
||||
}
|
||||
labelDescription={
|
||||
<>
|
||||
<p className="mb-2">
|
||||
<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}
|
||||
@@ -257,9 +259,11 @@ export const DealTicketMarginDetails = ({
|
||||
? liquidationEstimateWorstCaseIncludingBuyOrders
|
||||
: liquidationEstimateWorstCaseIncludingSellOrders;
|
||||
|
||||
// The estimate order query API gives us the liquidation price in formatted by asset decimals.
|
||||
// We need to calculate it with asset decimals, but display it with market decimals precision until the API changes.
|
||||
liquidationPriceEstimate = formatValue(
|
||||
liquidationEstimateWorstCase.toString(),
|
||||
market.decimalPlaces,
|
||||
assetDecimals,
|
||||
undefined,
|
||||
market.decimalPlaces
|
||||
);
|
||||
@@ -272,7 +276,7 @@ export const DealTicketMarginDetails = ({
|
||||
? liquidationEstimateBestCase
|
||||
: liquidationEstimateWorstCase
|
||||
).toString(),
|
||||
market.decimalPlaces,
|
||||
assetDecimals,
|
||||
undefined,
|
||||
market.decimalPlaces
|
||||
);
|
||||
@@ -304,7 +308,7 @@ export const DealTicketMarginDetails = ({
|
||||
key={'value-dropdown'}
|
||||
className="flex items-center justify-between w-full gap-2"
|
||||
>
|
||||
<div className="flex items-center text-left gap-1">
|
||||
<div className="flex items-center gap-1 text-left">
|
||||
<Tooltip description={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}>
|
||||
<span className="text-muted">{t('Margin required')}</span>
|
||||
</Tooltip>
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('DealTicket', () => {
|
||||
variables: {
|
||||
partyId: 'pubKey',
|
||||
filter: { liveOnly: true },
|
||||
pagination: { first: 1000 },
|
||||
pagination: { first: 5000 },
|
||||
},
|
||||
},
|
||||
result: {
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
Tooltip,
|
||||
TradingButton as Button,
|
||||
Pill,
|
||||
ExternalLink,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { useOpenVolume } from '@vegaprotocol/positions';
|
||||
@@ -78,7 +77,6 @@ import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
||||
import noop from 'lodash/noop';
|
||||
import { isNonPersistentOrder } from '../../utils/time-in-force-persistance';
|
||||
import { KeyValue } from './key-value';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
|
||||
export const REDUCE_ONLY_TOOLTIP =
|
||||
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.';
|
||||
@@ -275,9 +273,7 @@ export const DealTicket = ({
|
||||
|
||||
const assetSymbol = getAsset(market).symbol;
|
||||
|
||||
const baseQuote = getBaseQuoteUnit(
|
||||
market.tradableInstrument.instrument.metadata.tags
|
||||
);
|
||||
const assetUnit = getQuoteName(market);
|
||||
|
||||
const summaryError = useMemo(() => {
|
||||
if (!pubKey) {
|
||||
@@ -416,7 +412,7 @@ export const DealTicket = ({
|
||||
id="order-size"
|
||||
className="w-full"
|
||||
type="number"
|
||||
appendElement={baseQuote && <Pill size="xs">{baseQuote}</Pill>}
|
||||
appendElement={assetUnit && <Pill size="xs">{assetUnit}</Pill>}
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
data-testid="order-size"
|
||||
@@ -552,20 +548,15 @@ export const DealTicket = ({
|
||||
render={({ field }) => (
|
||||
<Tooltip
|
||||
description={
|
||||
<>
|
||||
<span>
|
||||
{disablePostOnlyCheckbox
|
||||
? t(
|
||||
'"Post only" can not be used on "Fill or Kill" or "Immediate or Cancel" orders.'
|
||||
)
|
||||
: t(
|
||||
'"Post only" will ensure the order is not filled immediately but is placed on the order book as a passive order. When the order is processed it is either stopped (if it would not be filled immediately), or placed in the order book as a passive order until the price taker matches with it.'
|
||||
)}
|
||||
</span>{' '}
|
||||
<ExternalLink href={DocsLinks?.POST_REDUCE_ONLY}>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>
|
||||
</>
|
||||
<span>
|
||||
{disablePostOnlyCheckbox
|
||||
? t(
|
||||
'"Post only" can not be used on "Fill or Kill" or "Immediate or Cancel" orders.'
|
||||
)
|
||||
: t(
|
||||
'"Post only" will ensure the order is not filled immediately but is placed on the order book as a passive order. When the order is processed it is either stopped (if it would not be filled immediately), or placed in the order book as a passive order until the price taker matches with it.'
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
@@ -589,18 +580,13 @@ export const DealTicket = ({
|
||||
render={({ field }) => (
|
||||
<Tooltip
|
||||
description={
|
||||
<>
|
||||
<span>
|
||||
{disableReduceOnlyCheckbox
|
||||
? t(
|
||||
'"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".'
|
||||
)
|
||||
: t(REDUCE_ONLY_TOOLTIP)}
|
||||
</span>{' '}
|
||||
<ExternalLink href={DocsLinks?.POST_REDUCE_ONLY}>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>
|
||||
</>
|
||||
<span>
|
||||
{disableReduceOnlyCheckbox
|
||||
? t(
|
||||
'"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".'
|
||||
)
|
||||
: t(REDUCE_ONLY_TOOLTIP)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
@@ -632,10 +618,7 @@ export const DealTicket = ({
|
||||
{t(`Trade only a fraction of the order size at once.
|
||||
After the peak size of the order has traded, the size is reset. This is repeated until the order is cancelled, expires, or its full volume trades away.
|
||||
For example, an iceberg order with a size of 1000 and a peak size of 100 will effectively be split into 10 orders with a size of 100 each.
|
||||
Note that the full volume of the order is not hidden and is still reflected in the order book.`)}{' '}
|
||||
<ExternalLink href={DocsLinks?.ICEBERG_ORDERS}>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>{' '}
|
||||
Note that the full volume of the order is not hidden and is still reflected in the order book.`)}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
@@ -685,7 +668,7 @@ export const DealTicket = ({
|
||||
subLabel={`${formatValue(
|
||||
normalizedOrder.size,
|
||||
market.positionDecimalPlaces
|
||||
)} ${baseQuote} @ ${
|
||||
)} ${assetUnit} @ ${
|
||||
type === Schema.OrderType.TYPE_MARKET
|
||||
? 'market'
|
||||
: `${formatValue(
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||