Feat/1446: Add UpdateAsset proposal form (#1558)

* Feat/1446: Add UpdateAsset proposal form

* Feat/1446: Formatting fix

* Feat/1446: changes from PR comments

* Feat/1446: Fix for mockWalletContext shape and lint fix

* Feat/1446: Fix more tests reliant on the updated mockPubkey shape change

* Feat/1446: Adding the update asset proposal terms interface into the new location

Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
Co-authored-by: Dexter <dexter.edwards93@gmail.com>
This commit is contained in:
Sam Keen 2022-10-05 16:07:42 +01:00 committed by GitHub
parent b2619eac7c
commit 46d03826cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 410 additions and 99 deletions

View File

@ -636,6 +636,7 @@
"NewMarketProposal": "New market proposal", "NewMarketProposal": "New market proposal",
"UpdateMarketProposal": "Update market proposal", "UpdateMarketProposal": "Update market proposal",
"NewAssetProposal": "New asset proposal", "NewAssetProposal": "New asset proposal",
"UpdateAssetProposal": "Update asset proposal",
"NewFreeformProposal": "New freeform proposal", "NewFreeformProposal": "New freeform proposal",
"NewRawProposal": "New raw proposal", "NewRawProposal": "New raw proposal",
"MinProposalRequirements": "You must have at least {{value}} VEGA associated to make a proposal", "MinProposalRequirements": "You must have at least {{value}} VEGA associated to make a proposal",
@ -647,6 +648,7 @@
"NewMarket": "New market", "NewMarket": "New market",
"UpdateMarket": "Update market", "UpdateMarket": "Update market",
"NewAsset": "New asset", "NewAsset": "New asset",
"UpdateAsset": "Update asset",
"Freeform": "Freeform", "Freeform": "Freeform",
"RawProposal": "Let me choose (raw proposal)", "RawProposal": "Let me choose (raw proposal)",
"UseMin": "Use minimum", "UseMin": "Use minimum",

View File

@ -26,7 +26,7 @@ import {
lastWeek, lastWeek,
nextWeek, nextWeek,
} from '../../test-helpers/mocks'; } from '../../test-helpers/mocks';
import type { ProposalsConnection_proposalsConnection_edges_node as ProposalNode } from '@vegaprotocol/governance'; import type { Proposals_proposalsConnection_edges_node as ProposalNode } from '../../proposals/__generated__/Proposals';
const renderComponent = ( const renderComponent = (
proposal: ProposalNode, proposal: ProposalNode,
@ -172,7 +172,7 @@ describe('Proposals list item details', () => {
datetime: lastWeek.toString(), datetime: lastWeek.toString(),
party: { party: {
__typename: 'Party', __typename: 'Party',
id: mockPubkey, id: mockPubkey.publicKey,
stakingSummary: { stakingSummary: {
__typename: 'StakingSummary', __typename: 'StakingSummary',
currentStakeAvailable: '1000', currentStakeAvailable: '1000',
@ -210,7 +210,7 @@ describe('Proposals list item details', () => {
datetime: lastWeek.toString(), datetime: lastWeek.toString(),
party: { party: {
__typename: 'Party', __typename: 'Party',
id: mockPubkey, id: mockPubkey.publicKey,
stakingSummary: { stakingSummary: {
__typename: 'StakingSummary', __typename: 'StakingSummary',
currentStakeAvailable: '1000', currentStakeAvailable: '1000',

View File

@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { ProposeFreeform } from './propose-freeform'; import { ProposeFreeform } from './propose-freeform';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { mockWalletContext } from '../../test-helpers/mocks'; import { mockWalletContext } from '../../test-helpers/mocks';
@ -81,16 +81,14 @@ describe('Propose Freeform', () => {
it('should render the title', async () => { it('should render the title', async () => {
renderComponent(); renderComponent();
await waitFor(() => expect(
expect(screen.getByText('New freeform proposal')).toBeInTheDocument() await screen.findByText('New freeform proposal')
); ).toBeInTheDocument();
}); });
it('should render the form components', async () => { it('should render the form components', async () => {
renderComponent(); renderComponent();
await waitFor(() => expect(await screen.findByTestId('freeform-proposal-form')).toBeTruthy();
expect(screen.getByTestId('freeform-proposal-form')).toBeTruthy()
);
expect(screen.getByTestId('min-proposal-requirements')).toBeTruthy(); expect(screen.getByTestId('min-proposal-requirements')).toBeTruthy();
expect(screen.getByTestId('proposal-docs-link')).toBeTruthy(); expect(screen.getByTestId('proposal-docs-link')).toBeTruthy();
expect(screen.getByTestId('proposal-title')).toBeTruthy(); expect(screen.getByTestId('proposal-title')).toBeTruthy();

View File

@ -27,7 +27,7 @@ export interface FreeformProposalFormFields {
proposalReference: string; proposalReference: string;
} }
const docsLink = 'freeform-proposal'; const DOCS_LINK = 'freeform-proposal';
export const ProposeFreeform = () => { export const ProposeFreeform = () => {
const { params, loading, error } = useNetworkParams([ const { params, loading, error } = useNetworkParams([
@ -76,9 +76,9 @@ export const ProposeFreeform = () => {
<p className="text-sm" data-testid="proposal-docs-link"> <p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span> <span className="mr-1">{t('ProposalTermsText')}</span>
<Link <Link
href={`${VEGA_DOCS_URL}/tutorials/proposals/${docsLink}`} href={`${VEGA_DOCS_URL}/tutorials/proposals/${DOCS_LINK}`}
target="_blank" target="_blank"
>{`${VEGA_DOCS_URL}/tutorials/proposals/${docsLink}`}</Link> >{`${VEGA_DOCS_URL}/tutorials/proposals/${DOCS_LINK}`}</Link>
</p> </p>
)} )}

View File

@ -1,4 +1,4 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import { ProposeNetworkParameter } from './propose-network-parameter'; import { ProposeNetworkParameter } from './propose-network-parameter';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { mockWalletContext } from '../../test-helpers/mocks'; import { mockWalletContext } from '../../test-helpers/mocks';
@ -81,16 +81,16 @@ describe('Propose Network Parameter', () => {
it('should render the correct title', async () => { it('should render the correct title', async () => {
renderComponent(); renderComponent();
await waitFor(() => expect(
expect(screen.getByText('Update network parameter proposal')).toBeTruthy() await screen.findByText('Update network parameter proposal')
); ).toBeTruthy();
}); });
it('should render the form components', async () => { it('should render the form components', async () => {
renderComponent(); renderComponent();
await waitFor(() => expect(
expect(screen.getByTestId('network-parameter-proposal-form')).toBeTruthy() await screen.findByTestId('network-parameter-proposal-form')
); ).toBeTruthy();
expect(screen.getByTestId('min-proposal-requirements')).toBeTruthy(); expect(screen.getByTestId('min-proposal-requirements')).toBeTruthy();
expect(screen.getByTestId('proposal-docs-link')).toBeTruthy(); expect(screen.getByTestId('proposal-docs-link')).toBeTruthy();
expect(screen.getByTestId('proposal-title')).toBeTruthy(); expect(screen.getByTestId('proposal-title')).toBeTruthy();
@ -103,15 +103,15 @@ describe('Propose Network Parameter', () => {
it('should render the network param select element with no initial value', async () => { it('should render the network param select element with no initial value', async () => {
renderComponent(); renderComponent();
await waitFor(() => expect(await screen.findByTestId('proposal-parameter-select')).toHaveValue(
expect(screen.getByTestId('proposal-parameter-select')).toHaveValue('') ''
); );
}); });
it('should render the current param value and a new value input when the network param select element is changed', async () => { it('should render the current param value and a new value input when the network param select element is changed', async () => {
renderComponent(); renderComponent();
await waitFor(() => expect(await screen.findByTestId('proposal-parameter-select')).toHaveValue(
expect(screen.getByTestId('proposal-parameter-select')).toHaveValue('') ''
); );
fireEvent.change(screen.getByTestId('proposal-parameter-select'), { fireEvent.change(screen.getByTestId('proposal-parameter-select'), {

View File

@ -69,7 +69,7 @@ export interface NetworkParameterProposalFormFields {
proposalReference: string; proposalReference: string;
} }
const docsLink = '/network-parameter-proposal'; const DOCS_LINK = '/network-parameter-proposal';
export const ProposeNetworkParameter = () => { export const ProposeNetworkParameter = () => {
const [selectedNetworkParam, setSelectedNetworkParam] = useState< const [selectedNetworkParam, setSelectedNetworkParam] = useState<
@ -141,9 +141,9 @@ export const ProposeNetworkParameter = () => {
<p className="text-sm" data-testid="proposal-docs-link"> <p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span> <span className="mr-1">{t('ProposalTermsText')}</span>
<Link <Link
href={`${VEGA_DOCS_URL}/tutorials/proposals${docsLink}`} href={`${VEGA_DOCS_URL}/tutorials/proposals${DOCS_LINK}`}
target="_blank" target="_blank"
>{`${VEGA_DOCS_URL}/tutorials/proposals${docsLink}`}</Link> >{`${VEGA_DOCS_URL}/tutorials/proposals${DOCS_LINK}`}</Link>
</p> </p>
)} )}

View File

@ -1,6 +1,6 @@
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter as Router } from 'react-router-dom'; import { MemoryRouter as Router } from 'react-router-dom';
import { render, screen, waitFor } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { VegaWalletContext } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { mockWalletContext } from '../../test-helpers/mocks'; import { mockWalletContext } from '../../test-helpers/mocks';
@ -81,16 +81,12 @@ describe('Propose New Asset', () => {
it('should render the title', async () => { it('should render the title', async () => {
renderComponent(); renderComponent();
await waitFor(() => expect(await screen.findByText('New asset proposal')).toBeTruthy();
expect(screen.getByText('New asset proposal')).toBeTruthy()
);
}); });
it('should render the form components', async () => { it('should render the form components', async () => {
renderComponent(); renderComponent();
await waitFor(() => expect(await screen.findByTestId('new-asset-proposal-form')).toBeTruthy();
expect(screen.getByTestId('new-asset-proposal-form')).toBeTruthy()
);
expect(screen.getByTestId('min-proposal-requirements')).toBeTruthy(); expect(screen.getByTestId('min-proposal-requirements')).toBeTruthy();
expect(screen.getByTestId('proposal-docs-link')).toBeTruthy(); expect(screen.getByTestId('proposal-docs-link')).toBeTruthy();
expect(screen.getByTestId('proposal-title')).toBeTruthy(); expect(screen.getByTestId('proposal-title')).toBeTruthy();

View File

@ -7,6 +7,7 @@ import {
getValidationTimestamp, getValidationTimestamp,
} from '@vegaprotocol/governance'; } from '@vegaprotocol/governance';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/react-helpers';
import { import {
ProposalFormMinRequirements, ProposalFormMinRequirements,
ProposalFormTitle, ProposalFormTitle,
@ -32,7 +33,7 @@ export interface NewAssetProposalFormFields {
proposalReference: string; proposalReference: string;
} }
const docsLink = '/new-asset-proposal'; const DOCS_LINK = '/new-asset-proposal';
export const ProposeNewAsset = () => { export const ProposeNewAsset = () => {
const { const {
@ -100,9 +101,9 @@ export const ProposeNewAsset = () => {
<p className="text-sm" data-testid="proposal-docs-link"> <p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span> <span className="mr-1">{t('ProposalTermsText')}</span>
<Link <Link
href={`${VEGA_DOCS_URL}/tutorials/proposals${docsLink}`} href={`${VEGA_DOCS_URL}/tutorials/proposals${DOCS_LINK}`}
target="_blank" target="_blank"
>{`${VEGA_DOCS_URL}/tutorials/proposals${docsLink}`}</Link> >{`${VEGA_DOCS_URL}/tutorials/proposals${DOCS_LINK}`}</Link>
</p> </p>
)} )}
@ -141,20 +142,11 @@ export const ProposeNewAsset = () => {
<ProposalFormTerms <ProposalFormTerms
registerField={register('proposalTerms', { registerField={register('proposalTerms', {
required: t('Required'), required: t('Required'),
validate: { validate: (value) => validateJson(value),
validateJson: (value) => {
try {
JSON.parse(value);
return true;
} catch (e) {
return t('Must be valid JSON');
}
},
},
})} })}
labelOverride={'Terms.newAsset (JSON format)'} labelOverride={'Terms.newAsset (JSON format)'}
errorMessage={errors?.proposalTerms?.message} errorMessage={errors?.proposalTerms?.message}
customDocLink={docsLink} customDocLink={DOCS_LINK}
/> />
<ProposalFormVoteAndEnactmentDeadline <ProposalFormVoteAndEnactmentDeadline

View File

@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { ProposeNewMarket } from './propose-new-market'; import { ProposeNewMarket } from './propose-new-market';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { mockWalletContext } from '../../test-helpers/mocks'; import { mockWalletContext } from '../../test-helpers/mocks';
@ -81,9 +81,7 @@ describe('Propose New Market', () => {
it('should render the form components', async () => { it('should render the form components', async () => {
renderComponent(); renderComponent();
await waitFor(() => expect(await screen.findByTestId('new-market-proposal-form')).toBeTruthy();
expect(screen.getByTestId('new-market-proposal-form')).toBeTruthy()
);
expect(screen.getByTestId('min-proposal-requirements')).toBeTruthy(); expect(screen.getByTestId('min-proposal-requirements')).toBeTruthy();
expect(screen.getByTestId('proposal-docs-link')).toBeTruthy(); expect(screen.getByTestId('proposal-docs-link')).toBeTruthy();
expect(screen.getByTestId('proposal-title')).toBeTruthy(); expect(screen.getByTestId('proposal-title')).toBeTruthy();

View File

@ -6,6 +6,7 @@ import {
getEnactmentTimestamp, getEnactmentTimestamp,
} from '@vegaprotocol/governance'; } from '@vegaprotocol/governance';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/react-helpers';
import { import {
ProposalFormMinRequirements, ProposalFormMinRequirements,
ProposalFormTitle, ProposalFormTitle,
@ -30,7 +31,7 @@ export interface NewMarketProposalFormFields {
proposalReference: string; proposalReference: string;
} }
const docsLink = '/new-market-proposal'; const DOCS_LINK = '/new-market-proposal';
export const ProposeNewMarket = () => { export const ProposeNewMarket = () => {
const { const {
@ -95,9 +96,9 @@ export const ProposeNewMarket = () => {
<p className="text-sm" data-testid="proposal-docs-link"> <p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span> <span className="mr-1">{t('ProposalTermsText')}</span>
<Link <Link
href={`${VEGA_DOCS_URL}/tutorials/proposals/${docsLink}`} href={`${VEGA_DOCS_URL}/tutorials/proposals${DOCS_LINK}`}
target="_blank" target="_blank"
>{`${VEGA_DOCS_URL}/tutorials/proposals/${docsLink}`}</Link> >{`${VEGA_DOCS_URL}/tutorials/proposals${DOCS_LINK}`}</Link>
</p> </p>
)} )}
@ -136,20 +137,11 @@ export const ProposeNewMarket = () => {
<ProposalFormTerms <ProposalFormTerms
registerField={register('proposalTerms', { registerField={register('proposalTerms', {
required: t('Required'), required: t('Required'),
validate: { validate: (value) => validateJson(value),
validateJson: (value) => {
try {
JSON.parse(value);
return true;
} catch (e) {
return t('Must be valid JSON');
}
},
},
})} })}
labelOverride={'Terms.newMarket (JSON format)'} labelOverride={'Terms.newMarket (JSON format)'}
errorMessage={errors?.proposalTerms?.message} errorMessage={errors?.proposalTerms?.message}
customDocLink={docsLink} customDocLink={DOCS_LINK}
/> />
<ProposalFormVoteAndEnactmentDeadline <ProposalFormVoteAndEnactmentDeadline

View File

@ -24,6 +24,7 @@ describe('Propose', () => {
expect(screen.getByText('New market')).toBeTruthy(); expect(screen.getByText('New market')).toBeTruthy();
expect(screen.getByText('Update market')).toBeTruthy(); expect(screen.getByText('Update market')).toBeTruthy();
expect(screen.getByText('New asset')).toBeTruthy(); expect(screen.getByText('New asset')).toBeTruthy();
expect(screen.getByText('Update asset')).toBeTruthy();
expect(screen.getByText('Freeform')).toBeTruthy(); expect(screen.getByText('Freeform')).toBeTruthy();
expect(screen.getByText('Let me choose (raw proposal)')).toBeTruthy(); expect(screen.getByText('Let me choose (raw proposal)')).toBeTruthy();
}); });

View File

@ -79,6 +79,16 @@ export const Propose = () => {
</Link> </Link>
</p> </p>
</li> </li>
<li>
<p>
<Link
className="underline"
to={`${Routes.GOVERNANCE}/propose/update-asset`}
>
{t('UpdateAsset')}
</Link>
</p>
</li>
<li> <li>
<p> <p>
<Link <Link

View File

@ -9,6 +9,7 @@ import {
Link, Link,
TextArea, TextArea,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { validateJson } from '@vegaprotocol/react-helpers';
import { useProposalSubmit } from '@vegaprotocol/governance'; import { useProposalSubmit } from '@vegaprotocol/governance';
import { import {
ProposalFormSubmit, ProposalFormSubmit,
@ -74,16 +75,7 @@ export const ProposeRaw = () => {
data-testid="proposal-data" data-testid="proposal-data"
{...register('rawProposalData', { {...register('rawProposalData', {
required: t('Required'), required: t('Required'),
validate: { validate: (value) => validateJson(value),
validateJson: (value) => {
try {
JSON.parse(value);
return true;
} catch (e) {
return t('Must be valid JSON');
}
},
},
})} })}
/> />
{errors.rawProposalData?.message && ( {errors.rawProposalData?.message && (

View File

@ -0,0 +1,4 @@
export {
ProposeUpdateAsset,
ProposeUpdateAsset as default,
} from './propose-update-asset';

View File

@ -0,0 +1,102 @@
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter as Router } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { mockWalletContext } from '../../test-helpers/mocks';
import { ProposeUpdateAsset } from './propose-update-asset';
import type { NetworkParamsQuery } from '@vegaprotocol/web3';
import type { MockedResponse } from '@apollo/client/testing';
import { NETWORK_PARAMETERS_QUERY } from '@vegaprotocol/react-helpers';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_DOCS_URL: 'https://docs.vega.xyz',
}),
}));
const updateAssetNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
request: {
query: NETWORK_PARAMETERS_QUERY,
},
result: {
data: {
networkParameters: [
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateAsset.maxClose',
value: '8760h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateAsset.maxEnact',
value: '8760h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateAsset.minClose',
value: '1h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateAsset.minEnact',
value: '2h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateAsset.minProposerBalance',
value: '1',
},
{
__typename: 'NetworkParameter',
key: 'spam.protection.proposal.min.tokens',
value: '1000000000000000000',
},
],
},
},
};
const renderComponent = () =>
render(
<Router>
<MockedProvider mocks={[updateAssetNetworkParamsQueryMock]}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposeUpdateAsset />
</VegaWalletContext.Provider>
</AppStateProvider>
</MockedProvider>
</Router>
);
// Note: form submission is tested in propose-raw.spec.tsx. Reusable form
// components are tested in their own directory.
describe('Propose Update Asset', () => {
it('should render successfully', async () => {
const { baseElement } = renderComponent();
await expect(baseElement).toBeTruthy();
});
it('should render the title', async () => {
renderComponent();
expect(await screen.findByText('Update asset proposal')).toBeTruthy();
});
it('should render the form components', async () => {
renderComponent();
expect(
await screen.findByTestId('update-asset-proposal-form')
).toBeTruthy();
expect(screen.getByTestId('min-proposal-requirements')).toBeTruthy();
expect(screen.getByTestId('proposal-docs-link')).toBeTruthy();
expect(screen.getByTestId('proposal-title')).toBeTruthy();
expect(screen.getByTestId('proposal-description')).toBeTruthy();
expect(screen.getByTestId('proposal-terms')).toBeTruthy();
expect(screen.getByTestId('proposal-vote-deadline')).toBeTruthy();
expect(screen.getByTestId('proposal-enactment-deadline')).toBeTruthy();
expect(screen.getByTestId('proposal-submit')).toBeTruthy();
expect(screen.getByTestId('proposal-transaction-dialog')).toBeTruthy();
});
});

View File

@ -0,0 +1,182 @@
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
useProposalSubmit,
getClosingTimestamp,
getEnactmentTimestamp,
} from '@vegaprotocol/governance';
import { useEnvironment } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/react-helpers';
import {
ProposalFormMinRequirements,
ProposalFormTitle,
ProposalFormDescription,
ProposalFormTerms,
ProposalFormSubmit,
ProposalFormTransactionDialog,
ProposalFormSubheader,
ProposalFormVoteAndEnactmentDeadline,
} from '../../components/propose';
import { AsyncRenderer, Link } from '@vegaprotocol/ui-toolkit';
import { Heading } from '../../../../components/heading';
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
import { NetworkParams, useNetworkParams } from '@vegaprotocol/react-helpers';
export interface UpdateAssetProposalFormFields {
proposalVoteDeadline: string;
proposalEnactmentDeadline: string;
proposalTitle: string;
proposalDescription: string;
proposalTerms: string;
proposalReference: string;
}
const DOCS_LINK = '/update-asset-proposal';
export const ProposeUpdateAsset = () => {
const {
params,
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams([
NetworkParams.governance_proposal_updateAsset_minClose,
NetworkParams.governance_proposal_updateAsset_maxClose,
NetworkParams.governance_proposal_updateAsset_minEnact,
NetworkParams.governance_proposal_updateAsset_maxEnact,
NetworkParams.governance_proposal_updateAsset_minProposerBalance,
NetworkParams.spam_protection_proposal_min_tokens,
]);
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
handleSubmit,
formState: { isSubmitting, errors },
} = useForm<UpdateAssetProposalFormFields>();
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
const onSubmit = async (fields: UpdateAssetProposalFormFields) => {
await submit({
rationale: {
title: fields.proposalTitle,
description: fields.proposalDescription,
},
terms: {
updateAsset: {
...JSON.parse(fields.proposalTerms),
},
closingTimestamp: getClosingTimestamp(fields.proposalVoteDeadline),
enactmentTimestamp: getEnactmentTimestamp(
fields.proposalVoteDeadline,
fields.proposalEnactmentDeadline
),
},
});
};
return (
<AsyncRenderer
loading={networkParamsLoading}
error={networkParamsError}
data={params}
>
<Heading title={t('UpdateAssetProposal')} />
<VegaWalletContainer>
{() => (
<>
<ProposalFormMinRequirements
minProposerBalance={
params.governance_proposal_updateAsset_minProposerBalance
}
spamProtectionMin={params.spam_protection_proposal_min_tokens}
/>
{VEGA_DOCS_URL && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<Link
href={`${VEGA_DOCS_URL}/tutorials/proposals${DOCS_LINK}`}
target="_blank"
>{`${VEGA_DOCS_URL}/tutorials/proposals${DOCS_LINK}`}</Link>
</p>
)}
{VEGA_EXPLORER_URL && (
<p className="text-sm">
{t('MoreAssetsInfo')}{' '}
<Link
href={`${VEGA_EXPLORER_URL}/assets`}
target="_blank"
>{`${VEGA_EXPLORER_URL}/assets`}</Link>
</p>
)}
<div data-testid="update-asset-proposal-form">
<form onSubmit={handleSubmit(onSubmit)}>
<ProposalFormSubheader>
{t('ProposalRationale')}
</ProposalFormSubheader>
<ProposalFormTitle
registerField={register('proposalTitle', {
required: t('Required'),
})}
errorMessage={errors?.proposalTitle?.message}
/>
<ProposalFormDescription
registerField={register('proposalDescription', {
required: t('Required'),
})}
errorMessage={errors?.proposalDescription?.message}
/>
<ProposalFormSubheader>
{t('UpdateAsset')}
</ProposalFormSubheader>
<ProposalFormTerms
registerField={register('proposalTerms', {
required: t('Required'),
validate: (value) => validateJson(value),
})}
labelOverride={'Terms.updateAsset (JSON format)'}
errorMessage={errors?.proposalTerms?.message}
customDocLink={DOCS_LINK}
/>
<ProposalFormVoteAndEnactmentDeadline
voteRegister={register('proposalVoteDeadline', {
required: t('Required'),
})}
voteErrorMessage={errors?.proposalVoteDeadline?.message}
voteMinClose={params.governance_proposal_updateAsset_minClose}
voteMaxClose={params.governance_proposal_updateAsset_maxClose}
enactmentRegister={register('proposalEnactmentDeadline', {
required: t('Required'),
})}
enactmentErrorMessage={
errors?.proposalEnactmentDeadline?.message
}
enactmentMinClose={
params.governance_proposal_updateAsset_minEnact
}
enactmentMaxClose={
params.governance_proposal_updateAsset_maxEnact
}
/>
<ProposalFormSubmit isSubmitting={isSubmitting} />
<ProposalFormTransactionDialog
finalizedProposal={finalizedProposal}
TransactionDialog={Dialog}
/>
</form>
</div>
</>
)}
</VegaWalletContainer>
</AsyncRenderer>
);
};

View File

@ -178,17 +178,17 @@ describe('Propose Update Market', () => {
it('should render the select element with no initial value', async () => { it('should render the select element with no initial value', async () => {
renderComponent(); renderComponent();
await waitFor(() => expect(
expect(screen.getByText('Update market proposal')).toBeInTheDocument() await screen.findByText('Update market proposal')
); ).toBeInTheDocument();
expect(screen.getByTestId('proposal-market-select')).toHaveValue(''); expect(screen.getByTestId('proposal-market-select')).toHaveValue('');
}); });
it('should render the correct market details when the market select is used', async () => { it('should render the correct market details when the market select is used', async () => {
renderComponent(); renderComponent();
await waitFor(() => expect(
expect(screen.getByText('Update market proposal')).toBeInTheDocument() await screen.findByText('Update market proposal')
); ).toBeInTheDocument();
fireEvent.change(screen.getByTestId('proposal-market-select'), { fireEvent.change(screen.getByTestId('proposal-market-select'), {
target: { target: {
value: value:

View File

@ -8,6 +8,7 @@ import {
getEnactmentTimestamp, getEnactmentTimestamp,
} from '@vegaprotocol/governance'; } from '@vegaprotocol/governance';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/react-helpers';
import { import {
ProposalFormSubheader, ProposalFormSubheader,
ProposalFormMinRequirements, ProposalFormMinRequirements,
@ -60,7 +61,7 @@ export interface UpdateMarketProposalFormFields {
proposalReference: string; proposalReference: string;
} }
const docsLink = '/update-market-proposal'; const DOCS_LINK = '/update-market-proposal';
export const ProposeUpdateMarket = () => { export const ProposeUpdateMarket = () => {
const { const {
@ -158,9 +159,9 @@ export const ProposeUpdateMarket = () => {
<p className="text-sm" data-testid="proposal-docs-link"> <p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span> <span className="mr-1">{t('ProposalTermsText')}</span>
<Link <Link
href={`${VEGA_DOCS_URL}/tutorials/proposals${docsLink}`} href={`${VEGA_DOCS_URL}/tutorials/proposals${DOCS_LINK}`}
target="_blank" target="_blank"
>{`${VEGA_DOCS_URL}/tutorials/proposals${docsLink}`}</Link> >{`${VEGA_DOCS_URL}/tutorials/proposals${DOCS_LINK}`}</Link>
</p> </p>
)} )}
@ -255,20 +256,11 @@ export const ProposeUpdateMarket = () => {
<ProposalFormTerms <ProposalFormTerms
registerField={register('proposalTerms', { registerField={register('proposalTerms', {
required: t('Required'), required: t('Required'),
validate: { validate: (value) => validateJson(value),
validateJson: (value) => {
try {
JSON.parse(value);
return true;
} catch (e) {
return t('Must be valid JSON');
}
},
},
})} })}
labelOverride={t('ProposeUpdateMarketTerms')} labelOverride={t('ProposeUpdateMarketTerms')}
errorMessage={errors?.proposalTerms?.message} errorMessage={errors?.proposalTerms?.message}
customDocLink={docsLink} customDocLink={DOCS_LINK}
/> />
<ProposalFormVoteAndEnactmentDeadline <ProposalFormVoteAndEnactmentDeadline

View File

@ -1,11 +1,15 @@
import { NETWORK_PARAMS_QUERY } from '@vegaprotocol/web3'; import { NETWORK_PARAMS_QUERY } from '@vegaprotocol/web3';
import type { MockedResponse } from '@apollo/client/testing'; import type { MockedResponse } from '@apollo/client/testing';
import type { NetworkParamsQuery } from '@vegaprotocol/web3'; import type { NetworkParamsQuery } from '@vegaprotocol/web3';
import type { PubKey } from '@vegaprotocol/wallet';
export const mockPubkey = '0x123'; export const mockPubkey: PubKey = {
publicKey: '0x123',
name: 'test key 1',
};
export const mockWalletContext = { export const mockWalletContext = {
pubKey: mockPubkey, pubKey: mockPubkey.publicKey,
pubKeys: [mockPubkey], pubKeys: [mockPubkey],
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)), sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(), connect: jest.fn(),

View File

@ -158,6 +158,13 @@ const LazyGovernanceProposeNewAsset = React.lazy(
) )
); );
const LazyGovernanceProposeUpdateAsset = React.lazy(
() =>
import(
/* webpackChunkName: "route-governance-propose-update-asset", webpackPrefetch: true */ './governance/propose/update-asset'
)
);
const LazyGovernanceProposeFreeform = React.lazy( const LazyGovernanceProposeFreeform = React.lazy(
() => () =>
import( import(
@ -273,6 +280,10 @@ const routerConfig = [
element: <LazyGovernanceProposeUpdateMarket />, element: <LazyGovernanceProposeUpdateMarket />,
}, },
{ path: 'new-asset', element: <LazyGovernanceProposeNewAsset /> }, { path: 'new-asset', element: <LazyGovernanceProposeNewAsset /> },
{
path: 'update-asset',
element: <LazyGovernanceProposeUpdateAsset />,
},
{ path: 'freeform', element: <LazyGovernanceProposeFreeform /> }, { path: 'freeform', element: <LazyGovernanceProposeFreeform /> },
{ path: 'raw', element: <LazyGovernanceProposeRaw /> }, { path: 'raw', element: <LazyGovernanceProposeRaw /> },
], ],

View File

@ -43,6 +43,14 @@ export const NetworkParams = {
governance_proposal_asset_maxClose: 'governance_proposal_asset_maxClose', governance_proposal_asset_maxClose: 'governance_proposal_asset_maxClose',
governance_proposal_asset_minEnact: 'governance_proposal_asset_minEnact', governance_proposal_asset_minEnact: 'governance_proposal_asset_minEnact',
governance_proposal_asset_maxEnact: 'governance_proposal_asset_maxEnact', governance_proposal_asset_maxEnact: 'governance_proposal_asset_maxEnact',
governance_proposal_updateAsset_minClose:
'governance_proposal_updateAsset_minClose',
governance_proposal_updateAsset_maxClose:
'governance_proposal_updateAsset_maxClose',
governance_proposal_updateAsset_minEnact:
'governance_proposal_updateAsset_minEnact',
governance_proposal_updateAsset_maxEnact:
'governance_proposal_updateAsset_maxEnact',
governance_proposal_updateNetParam_minClose: governance_proposal_updateNetParam_minClose:
'governance_proposal_updateNetParam_minClose', 'governance_proposal_updateNetParam_minClose',
governance_proposal_updateNetParam_maxClose: governance_proposal_updateNetParam_maxClose:
@ -71,6 +79,8 @@ export const NetworkParams = {
'governance_proposal_asset_requiredParticipation', 'governance_proposal_asset_requiredParticipation',
governance_proposal_asset_minProposerBalance: governance_proposal_asset_minProposerBalance:
'governance_proposal_asset_minProposerBalance', 'governance_proposal_asset_minProposerBalance',
governance_proposal_updateAsset_minProposerBalance:
'governance_proposal_updateAsset_minProposerBalance',
governance_proposal_updateNetParam_requiredMajority: governance_proposal_updateNetParam_requiredMajority:
'governance_proposal_updateNetParam_requiredMajority', 'governance_proposal_updateNetParam_requiredMajority',
governance_proposal_updateNetParam_requiredParticipation: governance_proposal_updateNetParam_requiredParticipation:

View File

@ -45,3 +45,12 @@ export const suitableForSyntaxHighlighter = (str: string) => {
return false; return false;
} }
}; };
export const validateJson = (value: string) => {
try {
JSON.parse(value);
return true;
} catch (e) {
return t('Must be valid JSON');
}
};

View File

@ -168,6 +168,21 @@ interface ProposalNewAssetTerms {
validationTimestamp: number; validationTimestamp: number;
} }
interface ProposalUpdateAssetTerms {
updateAsset: {
assetId: string;
changes: {
quantum: string;
erc20: {
withdrawThreshold: string;
lifetimeLimit: string;
};
};
};
closingTimestamp: number;
enactmentTimestamp: number;
}
interface OracleSpecBinding { interface OracleSpecBinding {
settlementPriceProperty: string; settlementPriceProperty: string;
tradingTerminationProperty: string; tradingTerminationProperty: string;
@ -227,7 +242,8 @@ export interface ProposalSubmission {
| ProposalNewMarketTerms | ProposalNewMarketTerms
| ProposalUpdateMarketTerms | ProposalUpdateMarketTerms
| ProposalNetworkParameterTerms | ProposalNetworkParameterTerms
| ProposalNewAssetTerms; | ProposalNewAssetTerms
| ProposalUpdateAssetTerms;
} }
export interface ProposalSubmissionBody { export interface ProposalSubmissionBody {