Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(governance): volume discount proposals #5022

Merged
merged 7 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/governance-e2e/.env
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ NX_SUCCESSOR_MARKETS=true
NX_PRODUCT_PERPETUALS=true
NX_REFERRALS=true
NX_UPDATE_MARKET_STATE=true
NX_VOLUME_DISCOUNTS=true

NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
Expand Down
1 change: 1 addition & 0 deletions apps/governance-e2e/.env.devnet
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_VOLUME_DISCOUNTS=true
1 change: 1 addition & 0 deletions apps/governance-e2e/.env.mainnet
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=false
NX_VOLUME_DISCOUNTS=false
1 change: 1 addition & 0 deletions apps/governance-e2e/.env.testnet
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_VOLUME_DISCOUNTS=true
1 change: 1 addition & 0 deletions apps/governance/.env
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=false
NX_GOVERNANCE_TRANSFERS=false
NX_VOLUME_DISCOUNTS=false
1 change: 1 addition & 0 deletions apps/governance/.env.capsule
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_VOLUME_DISCOUNTS=true
1 change: 1 addition & 0 deletions apps/governance/.env.devnet
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ NX_METAMASK_SNAPS=true
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_VOLUME_DISCOUNTS=true
1 change: 1 addition & 0 deletions apps/governance/.env.mainnet
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=false
NX_VOLUME_DISCOUNTS=false
1 change: 1 addition & 0 deletions apps/governance/.env.mainnet-mirror
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=false
NX_VOLUME_DISCOUNTS=false
1 change: 1 addition & 0 deletions apps/governance/.env.stagnet1
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_GOVERNANCE_TRANSFERS=true
NX_VOLUME_DISCOUNTS=true
1 change: 1 addition & 0 deletions apps/governance/.env.testnet
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ NX_METAMASK_SNAPS=true
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_VOLUME_DISCOUNTS=true
1 change: 1 addition & 0 deletions apps/governance/.env.validators-testnet
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=false
NX_VOLUME_DISCOUNTS=false
6 changes: 5 additions & 1 deletion apps/governance/src/i18n/translations/dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@
"UpdateMarketProposal": "Update market proposal",
"UpdateMarketStateProposal": "Update market state proposal",
"UpdateReferralProgramProposal": "Update referral program proposal",
"UpdateVolumeDiscountProgramProposal": "Update volume discount program proposal",
"MarketChange": "Market change",
"MarketStateChange": "Market state change",
"MarketDetails": "Market details",
Expand All @@ -734,6 +735,7 @@
"UpdateMarket": "Update market",
"UpdateMarketState": "Update market state",
"UpdateReferralProgram": "Update referral program",
"UpdateVolumeDiscountProgram": "Update volume discount program",
"NewAsset": "New asset",
"UpdateAsset": "Update asset",
"AssetID": "Asset ID",
Expand Down Expand Up @@ -910,5 +912,7 @@
"WindowLength": "Window length",
"WindowLengthDescription": "Number of epochs over which to evaluate a referral set's running volume",
"EndOfProgramTimestamp": "End of program",
"EndOfProgramTimestampDescription": "Time after which when the current epoch ends, the programs will end and benefits will be disabled."
"EndOfProgramTimestampDescription": "Time after which when the current epoch ends, the programs will end and benefits will be disabled.",
"BenefitTierVolumeDiscountFactor": "Volume discount factor",
"BenefitTierVolumeDiscountFactorDescription": "Discount given to those in this benefit tier"
}
1 change: 1 addition & 0 deletions apps/governance/src/routes/home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
includeUpdateVolumeDiscountPrograms: !!FLAGS.VOLUME_DISCOUNTS,
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ export const ProposalHeader = ({
fallbackTitle = t('UpdateReferralProgramProposal');
break;
}
case 'UpdateVolumeDiscountProgram': {
proposalType = 'UpdateVolumeDiscountProgram';
fallbackTitle = t('UpdateVolumeDiscountProgramProposal');
break;
}
case 'NewAsset': {
proposalType = 'NewAsset';
fallbackTitle = t('NewAssetProposal');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { render, screen } from '@testing-library/react';
import {
formatEndOfProgramTimestamp,
formatMinimumRunningNotionalTakerVolume,
formatReferralDiscountFactor,
formatReferralRewardFactor,
Expand All @@ -18,13 +17,16 @@ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
}),
}));

describe('ProposalReferralProgramDetails helper functions', () => {
it('should format end of program timestamp correctly', () => {
const input = '2023-01-01T12:00:00Z';
const formatted = formatEndOfProgramTimestamp(input);
expect(formatted).toBe('01 January 2023 12:00 (GMT)');
});
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});

afterEach(() => {
jest.useRealTimers();
});

describe('ProposalReferralProgramDetails helper functions', () => {
it('should format minimum running notional taker volume correctly', () => {
const input = '1000';
const formatted = formatMinimumRunningNotionalTakerVolume(input);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './proposal-volume-discount-program-details';
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { render, screen } from '@testing-library/react';
import { ProposalVolumeDiscountProgramDetails } from './proposal-volume-discount-program-details';
import { generateProposal } from '../../test-helpers/generate-proposals';

jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
appState: {
decimals: 2,
},
}),
}));

const mockReferralProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateVolumeDiscountProgram',
benefitTiers: [
{
minimumRunningNotionalTakerVolume: '10000',
volumeDiscountFactor: '0.05',
},
{
minimumRunningNotionalTakerVolume: '50000',
volumeDiscountFactor: '0.1',
},
{
minimumRunningNotionalTakerVolume: '100000',
volumeDiscountFactor: '0.15',
},
{
minimumRunningNotionalTakerVolume: '250000',
volumeDiscountFactor: '0.2',
},
{
minimumRunningNotionalTakerVolume: '500000',
volumeDiscountFactor: '0.25',
},
{
minimumRunningNotionalTakerVolume: '1000000',
volumeDiscountFactor: '0.3',
},
{
minimumRunningNotionalTakerVolume: '1500000',
volumeDiscountFactor: '0.35',
},
{
minimumRunningNotionalTakerVolume: '2000000',
volumeDiscountFactor: '0.4',
},
],
endOfProgramTimestamp: '1970-01-01T00:00:01.791568493Z',
windowLength: 7,
},
},
});

describe('ProposalVolumeDiscountProgramDetails', () => {
it('should not render if proposal is null', () => {
render(<ProposalVolumeDiscountProgramDetails proposal={null} />);
expect(
screen.queryByTestId('proposal-volume-discount-program-details')
).toBeNull();
});

it('should not render if __typename is not UpdateVolumeDiscountProgram', () => {
const updateMarketProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
},
},
});
render(
<ProposalVolumeDiscountProgramDetails proposal={updateMarketProposal} />
);
expect(
screen.queryByTestId('proposal-volume-discount-program-details')
).toBeNull();
});

it('should not render if there are no relevant fields', () => {
const incompleteProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateVolumeDiscountProgram',
},
},
});

render(
<ProposalVolumeDiscountProgramDetails proposal={incompleteProposal} />
);
expect(
screen.queryByTestId('proposal-volume-discount-program-details')
).toBeNull();
});

it('should render relevant fields if present', () => {
render(
<ProposalVolumeDiscountProgramDetails proposal={mockReferralProposal} />
);
expect(
screen.getByTestId('proposal-volume-discount-program-window-length')
).toBeInTheDocument();
expect(
screen.getByTestId(
'proposal-volume-discount-program-end-of-program-timestamp'
)
).toBeInTheDocument();
expect(
screen.getByTestId('proposal-volume-discount-program-benefit-tiers')
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
RoundedWrapper,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import {
formatEndOfProgramTimestamp,
formatMinimumRunningNotionalTakerVolume,
} from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';

interface ProposalReferralProgramDetailsProps {
proposal: ProposalQuery['proposal'];
}

export const formatVolumeDiscountFactor = (value: string) => {
return formatNumberPercentage(new BigNumber(value).times(100));
};

export const ProposalVolumeDiscountProgramDetails = ({
proposal,
}: ProposalReferralProgramDetailsProps) => {
const { t } = useTranslation();
if (proposal?.terms?.change?.__typename !== 'UpdateVolumeDiscountProgram') {
return null;
}

const benefitTiers = proposal?.terms?.change?.benefitTiers;
const windowLength = proposal?.terms?.change?.windowLength;
const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgramTimestamp;

if (!benefitTiers && !windowLength && !endOfProgramTimestamp) {
return null;
}

return (
<div data-testid="proposal-volume-discount-program-details">
<RoundedWrapper paddingBottom={true}>
{windowLength && (
<div data-testid="proposal-volume-discount-program-window-length">
<KeyValueTable>
<KeyValueTableRow>
<Tooltip description={t('WindowLengthDescription')}>
<span>{t('WindowLength')}</span>
</Tooltip>
{windowLength}
</KeyValueTableRow>
</KeyValueTable>
</div>
)}

{endOfProgramTimestamp && (
<div
className="mb-6"
data-testid="proposal-volume-discount-program-end-of-program-timestamp"
>
<KeyValueTable>
<KeyValueTableRow>
<Tooltip description={t('EndOfProgramTimestampDescription')}>
<span>{t('EndOfProgramTimestamp')}</span>
</Tooltip>
{formatEndOfProgramTimestamp(endOfProgramTimestamp)}
</KeyValueTableRow>
</KeyValueTable>
</div>
)}

{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.minimumRunningNotionalTakerVolume) -
Number(b.minimumRunningNotionalTakerVolume)
)
.map((benefitTier, index) => (
<div className="mb-4" key={index}>
<h4 className="font-semibold uppercase">
Tier {index + 1}
</h4>
{benefitTier.minimumRunningNotionalTakerVolume && (
<KeyValueTableRow>
<Tooltip
description={t(
'BenefitTierMinimumRunningNotionalTakerVolumeDescription'
)}
>
<span>
{t('BenefitTierMinimumRunningNotionalTakerVolume')}
</span>
</Tooltip>
{formatMinimumRunningNotionalTakerVolume(
benefitTier.minimumRunningNotionalTakerVolume
)}
</KeyValueTableRow>
)}
{benefitTier.volumeDiscountFactor && (
<KeyValueTableRow>
<Tooltip
description={t(
'BenefitTierVolumeDiscountFactorDescription'
)}
>
<span>{t('BenefitTierVolumeDiscountFactor')}</span>
</Tooltip>
{formatVolumeDiscountFactor(
benefitTier.volumeDiscountFactor
)}
</KeyValueTableRow>
)}
</div>
))}
</KeyValueTable>
</div>
)}
</RoundedWrapper>
</div>
);
};
Loading