* Feat/804: Routing and basic page setup * Feat/804: Type definition for proposals - awaiting asset docs/changes * Feat/804: New proposals lib * Feat/804: Propose form and page * Feat/804: Removing dud copied unit tests for now * Feat/804: Added types for new asset proposal * feat: handle new error types returned from wallet for proposals * chore: rename lib to governance * feat: move usage of hook into form component * feat: some adjustments and test coverage * chore: tidy up, remove tailwind config Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
parent
4c7ff15f23
commit
c40d71e1ed
@ -6,9 +6,9 @@ import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
|
||||
import { InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
DealTicketAmount,
|
||||
getDialogTitle,
|
||||
getDialogIntent,
|
||||
getDialogIcon,
|
||||
getOrderDialogTitle,
|
||||
getOrderDialogIntent,
|
||||
getOrderDialogIcon,
|
||||
MarketSelector,
|
||||
} from '@vegaprotocol/deal-ticket';
|
||||
import type { Order } from '@vegaprotocol/orders';
|
||||
@ -143,9 +143,9 @@ export const DealTicketSteps = ({
|
||||
partyData={partyData}
|
||||
/>
|
||||
<TransactionDialog
|
||||
title={getDialogTitle(finalizedOrder?.status)}
|
||||
intent={getDialogIntent(finalizedOrder?.status)}
|
||||
icon={getDialogIcon(finalizedOrder?.status)}
|
||||
title={getOrderDialogTitle(finalizedOrder?.status)}
|
||||
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
||||
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
||||
>
|
||||
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
||||
</TransactionDialog>
|
||||
|
@ -157,6 +157,9 @@
|
||||
"Once unlocked they can be redeemed from the contract so that you can transfer them between wallets.": "Once unlocked they can be redeemed from the contract so that you can transfer them between wallets.",
|
||||
"Tokens are held in different <trancheLink>Tranches</trancheLink>. Each tranche has its own schedule for how the tokens are unlocked.": "Tokens are held in different <trancheLink>Tranches</trancheLink>. Each tranche has its own schedule for how the tokens are unlocked.",
|
||||
"proposals": "Proposals",
|
||||
"proposal": "Proposal",
|
||||
"submitting": "Submitting",
|
||||
"submit": "Submit",
|
||||
"proposedEnactment": "Proposed enactment",
|
||||
"Enacted": "Enacted",
|
||||
"enactedOn": "Enacted on",
|
||||
@ -620,5 +623,7 @@
|
||||
"InvalidAssetDetails": "Invalid asset details",
|
||||
"FilterProposals": "Filter proposals",
|
||||
"FilterProposalsDescription": "Filter by proposal ID or proposer ID",
|
||||
"Freeform proposal": "Freeform proposal"
|
||||
"Freeform proposal": "Freeform proposal",
|
||||
"NewProposal": "New proposal",
|
||||
"MinProposalRequirements": "You must have at least 1 VEGA to make a proposal"
|
||||
}
|
||||
|
1
apps/token/src/routes/governance/propose/index.tsx
Normal file
1
apps/token/src/routes/governance/propose/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { Propose } from './propose';
|
21
apps/token/src/routes/governance/propose/propose.tsx
Normal file
21
apps/token/src/routes/governance/propose/propose.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ProposalForm } from '@vegaprotocol/governance';
|
||||
import { Heading } from '../../../components/heading';
|
||||
import { VegaWalletContainer } from '../../../components/vega-wallet-container';
|
||||
|
||||
export const Propose = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Heading title={t('NewProposal')} />
|
||||
<VegaWalletContainer>
|
||||
{() => (
|
||||
<>
|
||||
<p>{t('MinProposalRequirements')}</p>
|
||||
<ProposalForm />
|
||||
</>
|
||||
)}
|
||||
</VegaWalletContainer>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { ProposalContainer } from './governance/proposal';
|
||||
import { ProposalsContainer } from './governance/proposals';
|
||||
import { Propose } from './governance/propose';
|
||||
|
||||
import Home from './home';
|
||||
import NotFound from './not-found';
|
||||
@ -165,6 +166,7 @@ const routerConfig = [
|
||||
component: LazyGovernance,
|
||||
children: [
|
||||
{ path: ':proposalId', element: <ProposalContainer /> },
|
||||
{ path: 'propose', element: <Propose /> },
|
||||
{ index: true, element: <ProposalsContainer /> },
|
||||
],
|
||||
},
|
||||
|
@ -34,9 +34,9 @@ export const DealTicketManager = ({
|
||||
/>
|
||||
)}
|
||||
<TransactionDialog
|
||||
title={getDialogTitle(finalizedOrder?.status)}
|
||||
intent={getDialogIntent(finalizedOrder?.status)}
|
||||
icon={getDialogIcon(finalizedOrder?.status)}
|
||||
title={getOrderDialogTitle(finalizedOrder?.status)}
|
||||
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
||||
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
||||
>
|
||||
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
||||
</TransactionDialog>
|
||||
@ -44,7 +44,9 @@ export const DealTicketManager = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const getDialogTitle = (status?: OrderStatus): string | undefined => {
|
||||
export const getOrderDialogTitle = (
|
||||
status?: OrderStatus
|
||||
): string | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
@ -63,7 +65,9 @@ export const getDialogTitle = (status?: OrderStatus): string | undefined => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getDialogIntent = (status?: OrderStatus): Intent | undefined => {
|
||||
export const getOrderDialogIntent = (
|
||||
status?: OrderStatus
|
||||
): Intent | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
@ -81,7 +85,9 @@ export const getDialogIntent = (status?: OrderStatus): Intent | undefined => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getDialogIcon = (status?: OrderStatus): ReactNode | undefined => {
|
||||
export const getOrderDialogIcon = (
|
||||
status?: OrderStatus
|
||||
): ReactNode | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
12
libs/governance/.babelrc
Normal file
12
libs/governance/.babelrc
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nrwl/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
18
libs/governance/.eslintrc.json
Normal file
18
libs/governance/.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*", "__generated__"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
7
libs/governance/README.md
Normal file
7
libs/governance/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# governance
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test governance` to execute the unit tests via [Jest](https://jestjs.io).
|
10
libs/governance/jest.config.js
Normal file
10
libs/governance/jest.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
displayName: 'governance',
|
||||
preset: '../../jest.preset.js',
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': 'babel-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/governance',
|
||||
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||
};
|
4
libs/governance/package.json
Normal file
4
libs/governance/package.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "@vegaprotocol/governance",
|
||||
"version": "0.0.1"
|
||||
}
|
43
libs/governance/project.json
Normal file
43
libs/governance/project.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"root": "libs/governance",
|
||||
"sourceRoot": "libs/governance/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nrwl/web:rollup",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/governance",
|
||||
"tsConfig": "libs/governance/tsconfig.lib.json",
|
||||
"project": "libs/governance/package.json",
|
||||
"entryFile": "libs/governance/src/index.ts",
|
||||
"external": ["react/jsx-runtime"],
|
||||
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
|
||||
"compiler": "babel",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "libs/governance/README.md",
|
||||
"input": ".",
|
||||
"output": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nrwl/linter:eslint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["libs/governance/**/*.{ts,tsx,js,jsx}"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nrwl/jest:jest",
|
||||
"outputs": ["coverage/libs/governance"],
|
||||
"options": {
|
||||
"jestConfig": "libs/governance/jest.config.js",
|
||||
"passWithNoTests": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
2
libs/governance/src/index.ts
Normal file
2
libs/governance/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './lib';
|
||||
export * from './utils';
|
2
libs/governance/src/lib/index.ts
Normal file
2
libs/governance/src/lib/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './proposals-hooks';
|
||||
export * from './proposal-form';
|
175
libs/governance/src/lib/proposal-form.spec.tsx
Normal file
175
libs/governance/src/lib/proposal-form.spec.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
BusEventType,
|
||||
ProposalRejectionReason,
|
||||
ProposalState,
|
||||
} from '@vegaprotocol/types';
|
||||
import { ProposalForm } from './proposal-form';
|
||||
import { PROPOSAL_EVENT_SUB } from './proposals-hooks';
|
||||
import type { ProposalEvent } from './proposals-hooks/__generated__/ProposalEvent';
|
||||
|
||||
describe('ProposalForm', () => {
|
||||
const pubkey = '0x123';
|
||||
const mockProposalEvent: MockedResponse<ProposalEvent> = {
|
||||
request: {
|
||||
query: PROPOSAL_EVENT_SUB,
|
||||
variables: {
|
||||
partyId: pubkey,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
busEvents: [
|
||||
{
|
||||
__typename: 'BusEvent',
|
||||
type: BusEventType.Proposal,
|
||||
event: {
|
||||
__typename: 'Proposal',
|
||||
id: '2fca514cebf9f465ae31ecb4c5721e3a6f5f260425ded887ca50ba15b81a5d50',
|
||||
reference: 'proposal-reference',
|
||||
state: ProposalState.Open,
|
||||
rejectionReason: ProposalRejectionReason.CloseTimeTooLate,
|
||||
errorDetails: 'error-details',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
delay: 300,
|
||||
};
|
||||
const setup = (mockSendTx = jest.fn()) => {
|
||||
return render(
|
||||
<MockedProvider mocks={[mockProposalEvent]}>
|
||||
<VegaWalletContext.Provider
|
||||
value={
|
||||
{
|
||||
keypair: { pub: pubkey },
|
||||
sendTx: mockSendTx,
|
||||
} as unknown as VegaWalletContextShape
|
||||
}
|
||||
>
|
||||
<ProposalForm />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('handles validation', async () => {
|
||||
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve());
|
||||
setup(mockSendTx);
|
||||
|
||||
fireEvent.click(screen.getByTestId('proposal-submit'));
|
||||
expect(mockSendTx).not.toHaveBeenCalled();
|
||||
|
||||
expect(await screen.findByTestId('input-error-text')).toHaveTextContent(
|
||||
'Required'
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByTestId('proposal-data'), {
|
||||
target: { value: 'invalid' },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId('proposal-submit'));
|
||||
expect(mockSendTx).not.toHaveBeenCalled();
|
||||
|
||||
expect(await screen.findByTestId('input-error-text')).toHaveTextContent(
|
||||
'Must be valid JSON'
|
||||
);
|
||||
});
|
||||
|
||||
it('sends the transaction', async () => {
|
||||
const mockSendTx = jest.fn().mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
setTimeout(
|
||||
() =>
|
||||
resolve({
|
||||
txHash: 'tx-hash',
|
||||
tx: {
|
||||
signature: {
|
||||
value:
|
||||
'cfe592d169f87d0671dd447751036d0dddc165b9c4b65e5a5060e2bbadd1aa726d4cbe9d3c3b327bcb0bff4f83999592619a2493f9bbd251fae99ce7ce766909',
|
||||
},
|
||||
},
|
||||
}),
|
||||
100
|
||||
);
|
||||
})
|
||||
);
|
||||
setup(mockSendTx);
|
||||
|
||||
const inputJSON = '{}';
|
||||
fireEvent.change(screen.getByTestId('proposal-data'), {
|
||||
target: { value: inputJSON },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('proposal-submit'));
|
||||
});
|
||||
|
||||
expect(mockSendTx).toHaveBeenCalledWith({
|
||||
propagate: true,
|
||||
pubKey: pubkey,
|
||||
proposalSubmission: JSON.parse(inputJSON),
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent(
|
||||
'Confirm transaction in wallet'
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(200);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent(
|
||||
'Awaiting network confirmation'
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(400);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent(
|
||||
'Proposal submitted'
|
||||
);
|
||||
});
|
||||
|
||||
it('can be rejected by the user', async () => {
|
||||
const mockSendTx = jest.fn().mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => resolve(null), 100);
|
||||
})
|
||||
);
|
||||
setup(mockSendTx);
|
||||
|
||||
const inputJSON = '{}';
|
||||
fireEvent.change(screen.getByTestId('proposal-data'), {
|
||||
target: { value: inputJSON },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('proposal-submit'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent(
|
||||
'Confirm transaction in wallet'
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(200);
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
81
libs/governance/src/lib/proposal-form.tsx
Normal file
81
libs/governance/src/lib/proposal-form.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
InputError,
|
||||
TextArea,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useProposalSubmit } from './proposals-hooks';
|
||||
import {
|
||||
getProposalDialogIcon,
|
||||
getProposalDialogIntent,
|
||||
getProposalDialogTitle,
|
||||
} from '../utils';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export interface FormFields {
|
||||
proposalData: string;
|
||||
}
|
||||
|
||||
export const ProposalForm = () => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm<FormFields>();
|
||||
const { finalizedProposal, submit, TransactionDialog } = useProposalSubmit();
|
||||
|
||||
const hasError = Boolean(errors.proposalData?.message);
|
||||
|
||||
const onSubmit = async (fields: FormFields) => {
|
||||
await submit(JSON.parse(fields.proposalData));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormGroup
|
||||
label="Make a proposal by submitting JSON"
|
||||
labelFor="proposal-data"
|
||||
>
|
||||
<TextArea
|
||||
id="proposal-data"
|
||||
className="min-h-[200px]"
|
||||
hasError={hasError}
|
||||
data-testid="proposal-data"
|
||||
{...register('proposalData', {
|
||||
required: t('Required'),
|
||||
validate: {
|
||||
validateJson: (value) => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return t('Must be valid JSON');
|
||||
}
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.proposalData?.message && (
|
||||
<InputError intent="danger" className="mt-4">
|
||||
{errors.proposalData?.message}
|
||||
</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="my-20"
|
||||
data-testid="proposal-submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? t('Submitting') : t('Submit')} {t('Proposal')}
|
||||
</Button>
|
||||
<TransactionDialog
|
||||
title={getProposalDialogTitle(finalizedProposal?.state)}
|
||||
intent={getProposalDialogIntent(finalizedProposal?.state)}
|
||||
icon={getProposalDialogIcon(finalizedProposal?.state)}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
63
libs/governance/src/lib/proposals-hooks/__generated__/ProposalEvent.ts
generated
Normal file
63
libs/governance/src/lib/proposals-hooks/__generated__/ProposalEvent.ts
generated
Normal file
@ -0,0 +1,63 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { BusEventType, ProposalState, ProposalRejectionReason } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL subscription operation: ProposalEvent
|
||||
// ====================================================
|
||||
|
||||
export interface ProposalEvent_busEvents_event_TimeUpdate {
|
||||
__typename: "TimeUpdate" | "MarketEvent" | "TransferResponses" | "PositionResolution" | "Order" | "Trade" | "Account" | "Party" | "MarginLevels" | "Vote" | "MarketData" | "NodeSignature" | "LossSocialization" | "SettlePosition" | "Market" | "Asset" | "MarketTick" | "SettleDistressed" | "AuctionEvent" | "RiskFactor" | "Deposit" | "Withdrawal" | "OracleSpec" | "LiquidityProvision";
|
||||
}
|
||||
|
||||
export interface ProposalEvent_busEvents_event_Proposal {
|
||||
__typename: "Proposal";
|
||||
/**
|
||||
* Proposal ID that is filled by VEGA once proposal reaches the network
|
||||
*/
|
||||
id: string | null;
|
||||
/**
|
||||
* A UUID reference to aid tracking proposals on VEGA
|
||||
*/
|
||||
reference: string;
|
||||
/**
|
||||
* State of the proposal
|
||||
*/
|
||||
state: ProposalState;
|
||||
/**
|
||||
* Reason for the proposal to be rejected by the core
|
||||
*/
|
||||
rejectionReason: ProposalRejectionReason | null;
|
||||
/**
|
||||
* Error details of the rejectionReason
|
||||
*/
|
||||
errorDetails: string | null;
|
||||
}
|
||||
|
||||
export type ProposalEvent_busEvents_event = ProposalEvent_busEvents_event_TimeUpdate | ProposalEvent_busEvents_event_Proposal;
|
||||
|
||||
export interface ProposalEvent_busEvents {
|
||||
__typename: "BusEvent";
|
||||
/**
|
||||
* the type of event we're dealing with
|
||||
*/
|
||||
type: BusEventType;
|
||||
/**
|
||||
* the payload - the wrapped event
|
||||
*/
|
||||
event: ProposalEvent_busEvents_event;
|
||||
}
|
||||
|
||||
export interface ProposalEvent {
|
||||
/**
|
||||
* Subscribe to event data from the event bus
|
||||
*/
|
||||
busEvents: ProposalEvent_busEvents[] | null;
|
||||
}
|
||||
|
||||
export interface ProposalEventVariables {
|
||||
partyId: string;
|
||||
}
|
3
libs/governance/src/lib/proposals-hooks/index.ts
Normal file
3
libs/governance/src/lib/proposals-hooks/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './__generated__/ProposalEvent';
|
||||
export * from './use-proposal-event';
|
||||
export * from './use-proposal-submit';
|
@ -0,0 +1,75 @@
|
||||
import { useApolloClient, gql } from '@apollo/client';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import type {
|
||||
ProposalEvent,
|
||||
ProposalEventVariables,
|
||||
ProposalEvent_busEvents_event_Proposal,
|
||||
} from './__generated__/ProposalEvent';
|
||||
import type { Subscription } from 'zen-observable-ts';
|
||||
|
||||
export const PROPOSAL_EVENT_SUB = gql`
|
||||
subscription ProposalEvent($partyId: ID!) {
|
||||
busEvents(partyId: $partyId, batchSize: 0, types: [Proposal]) {
|
||||
type
|
||||
event {
|
||||
... on Proposal {
|
||||
id
|
||||
reference
|
||||
state
|
||||
rejectionReason
|
||||
errorDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const useProposalEvent = () => {
|
||||
const client = useApolloClient();
|
||||
const subRef = useRef<Subscription | null>(null);
|
||||
|
||||
const waitForProposalEvent = useCallback(
|
||||
(
|
||||
id: string,
|
||||
partyId: string,
|
||||
callback: (proposal: ProposalEvent_busEvents_event_Proposal) => void
|
||||
) => {
|
||||
subRef.current = client
|
||||
.subscribe<ProposalEvent, ProposalEventVariables>({
|
||||
query: PROPOSAL_EVENT_SUB,
|
||||
variables: { partyId },
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
if (!data?.busEvents?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No types available for the subscription result
|
||||
const matchingProposalEvent = data.busEvents.find((e) => {
|
||||
if (e.event.__typename !== 'Proposal') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return e.event.id === id;
|
||||
});
|
||||
|
||||
if (
|
||||
matchingProposalEvent &&
|
||||
matchingProposalEvent.event.__typename === 'Proposal'
|
||||
) {
|
||||
callback(matchingProposalEvent.event);
|
||||
subRef.current?.unsubscribe();
|
||||
}
|
||||
});
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
subRef.current?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return waitForProposalEvent;
|
||||
};
|
@ -0,0 +1,58 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { useVegaWallet, useVegaTransaction } from '@vegaprotocol/wallet';
|
||||
import { determineId } from '@vegaprotocol/react-helpers';
|
||||
import { useProposalEvent } from './use-proposal-event';
|
||||
import type { ProposalSubmission } from '@vegaprotocol/wallet';
|
||||
import type { ProposalEvent_busEvents_event_Proposal } from './__generated__/ProposalEvent';
|
||||
|
||||
export const useProposalSubmit = () => {
|
||||
const { keypair } = useVegaWallet();
|
||||
const waitForProposalEvent = useProposalEvent();
|
||||
|
||||
const { send, transaction, setComplete, TransactionDialog } =
|
||||
useVegaTransaction();
|
||||
|
||||
const [finalizedProposal, setFinalizedProposal] =
|
||||
useState<ProposalEvent_busEvents_event_Proposal | null>(null);
|
||||
|
||||
const submit = useCallback(
|
||||
async (proposal: ProposalSubmission) => {
|
||||
if (!keypair || !proposal) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFinalizedProposal(null);
|
||||
|
||||
try {
|
||||
const res = await send({
|
||||
pubKey: keypair.pub,
|
||||
propagate: true,
|
||||
proposalSubmission: proposal,
|
||||
});
|
||||
|
||||
if (res?.signature) {
|
||||
const resId = determineId(res.signature);
|
||||
if (resId) {
|
||||
waitForProposalEvent(resId, keypair.pub, (p) => {
|
||||
setFinalizedProposal(p);
|
||||
setComplete();
|
||||
});
|
||||
}
|
||||
}
|
||||
return res;
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[keypair, send, setComplete, waitForProposalEvent]
|
||||
);
|
||||
|
||||
return {
|
||||
transaction,
|
||||
finalizedProposal,
|
||||
TransactionDialog,
|
||||
submit,
|
||||
};
|
||||
};
|
1
libs/governance/src/setup-tests.ts
Normal file
1
libs/governance/src/setup-tests.ts
Normal file
@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
1
libs/governance/src/utils/index.ts
Normal file
1
libs/governance/src/utils/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './proposal-dialog-helpers';
|
74
libs/governance/src/utils/proposal-dialog-helpers.tsx
Normal file
74
libs/governance/src/utils/proposal-dialog-helpers.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Icon, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const getProposalDialogTitle = (
|
||||
status?: ProposalState
|
||||
): string | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case ProposalState.Open:
|
||||
return t('Proposal submitted');
|
||||
case ProposalState.WaitingForNodeVote:
|
||||
return t('Proposal waiting for node vote');
|
||||
case ProposalState.Passed:
|
||||
return t('Proposal passed');
|
||||
case ProposalState.Enacted:
|
||||
return t('Proposal enacted');
|
||||
case ProposalState.Declined:
|
||||
return t('Proposal declined');
|
||||
case ProposalState.Rejected:
|
||||
return t('Proposal rejected');
|
||||
case ProposalState.Failed:
|
||||
return t('Proposal failed');
|
||||
default:
|
||||
return t('Submission failed');
|
||||
}
|
||||
};
|
||||
|
||||
export const getProposalDialogIntent = (
|
||||
status?: ProposalState
|
||||
): Intent | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case ProposalState.Passed:
|
||||
case ProposalState.Enacted:
|
||||
return Intent.Success;
|
||||
case ProposalState.Open:
|
||||
case ProposalState.WaitingForNodeVote:
|
||||
return Intent.None;
|
||||
case ProposalState.Rejected:
|
||||
case ProposalState.Failed:
|
||||
case ProposalState.Declined:
|
||||
return Intent.Danger;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const getProposalDialogIcon = (
|
||||
status?: ProposalState
|
||||
): ReactNode | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case ProposalState.Passed:
|
||||
case ProposalState.Enacted:
|
||||
return <Icon name="tick" size={20} />;
|
||||
case ProposalState.Rejected:
|
||||
case ProposalState.Failed:
|
||||
case ProposalState.Declined:
|
||||
return <Icon name="error" size={20} />;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
25
libs/governance/tsconfig.json
Normal file
25
libs/governance/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
26
libs/governance/tsconfig.lib.json
Normal file
26
libs/governance/tsconfig.lib.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": ["node"]
|
||||
},
|
||||
"files": [
|
||||
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||
"../../node_modules/@nrwl/react/typings/image.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.test.jsx",
|
||||
"**/*.stories.ts",
|
||||
"**/*.stories.js",
|
||||
"**/*.stories.jsx",
|
||||
"**/*.stories.tsx"
|
||||
],
|
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||
}
|
19
libs/governance/tsconfig.spec.json
Normal file
19
libs/governance/tsconfig.spec.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.jsx",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
@ -219,10 +219,15 @@ export class RestConnector implements VegaConnector {
|
||||
}
|
||||
|
||||
if ('errors' in err) {
|
||||
return err.errors['*'].join(', ');
|
||||
const result = Object.entries(err.errors)
|
||||
.map((entry) => {
|
||||
return `${entry[0]}: ${entry[1].join(' | ')}`;
|
||||
})
|
||||
.join(', ');
|
||||
return result;
|
||||
}
|
||||
|
||||
return t("Something wen't wrong");
|
||||
return t('Something went wrong');
|
||||
}
|
||||
|
||||
private async request(
|
||||
@ -240,9 +245,10 @@ export class RestConnector implements VegaConnector {
|
||||
|
||||
if (!fetchResult.ok) {
|
||||
const errorData = await fetchResult.json();
|
||||
const error = this.parseError(errorData);
|
||||
return {
|
||||
status: fetchResult.status,
|
||||
error: this.parseError(errorData),
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -79,6 +79,170 @@ export interface WithdrawSubmissionBody extends BaseTransaction {
|
||||
};
|
||||
}
|
||||
|
||||
interface ProposalNewMarketTerms {
|
||||
newMarket: {
|
||||
changes: {
|
||||
decimalPlaces: string;
|
||||
positionDecimalPlaces: string;
|
||||
instrument: {
|
||||
name: string;
|
||||
code: string;
|
||||
future: {
|
||||
settlementAsset: string;
|
||||
quoteName: string;
|
||||
settlementPriceDecimals: number;
|
||||
oracleSpecForSettlementPrice: OracleSpecFor;
|
||||
oracleSpecForTradingTermination: OracleSpecFor;
|
||||
oracleSpecBinding: OracleSpecBinding;
|
||||
};
|
||||
};
|
||||
metadata?: string[];
|
||||
priceMonitoringParameters?: PriceMonitoringParameters;
|
||||
liquidityMonitoringParameters?: {
|
||||
targetStakeParameters: {
|
||||
timeWindow: string;
|
||||
scalingFactor: number;
|
||||
};
|
||||
triggeringRatio: number;
|
||||
auctionExtension: string;
|
||||
};
|
||||
logNormal: LogNormal;
|
||||
};
|
||||
liquidityCommitment: {
|
||||
commitmentAmount: string;
|
||||
fee: string;
|
||||
buys: Buy[];
|
||||
sells: Buy[];
|
||||
};
|
||||
};
|
||||
closingTimestamp: number;
|
||||
enactmentTimestamp: number;
|
||||
}
|
||||
|
||||
interface ProposalUpdateMarketTerms {
|
||||
updateMarket: {
|
||||
marketId: string;
|
||||
changes: {
|
||||
instrument: {
|
||||
code: string;
|
||||
future: {
|
||||
quoteName: string;
|
||||
settlementPriceDecimals: number;
|
||||
oracleSpecForSettlementPrice: OracleSpecFor;
|
||||
oracleSpecForTradingTermination: OracleSpecFor;
|
||||
oracleSpecBinding: OracleSpecBinding;
|
||||
};
|
||||
};
|
||||
priceMonitoringParameters?: PriceMonitoringParameters;
|
||||
logNormal: LogNormal;
|
||||
};
|
||||
};
|
||||
closingTimestamp: number;
|
||||
enactmentTimestamp: number;
|
||||
}
|
||||
|
||||
interface ProposalNetworkParameterTerms {
|
||||
updateNetworkParameter: {
|
||||
changes: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
closingTimestamp: number;
|
||||
enactmentTimestamp: number;
|
||||
}
|
||||
|
||||
interface ProposalFreeformTerms {
|
||||
newFreeform: Record<string, never>;
|
||||
closingTimestamp: number;
|
||||
}
|
||||
|
||||
interface ProposalNewAssetTerms {
|
||||
newAsset: {
|
||||
changes: {
|
||||
name: string;
|
||||
symbol: string;
|
||||
totalSupply: string;
|
||||
decimals: string;
|
||||
quantum: string;
|
||||
erc20: {
|
||||
contractAddress: string;
|
||||
withdrawThreshold: string;
|
||||
lifetimeLimit: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
closingTimestamp: number;
|
||||
enactmentTimestamp: number;
|
||||
}
|
||||
|
||||
interface OracleSpecBinding {
|
||||
settlementPriceProperty: string;
|
||||
tradingTerminationProperty: string;
|
||||
}
|
||||
|
||||
interface OracleSpecFor {
|
||||
pubKeys: string[];
|
||||
filters: Filter[];
|
||||
}
|
||||
|
||||
interface Filter {
|
||||
key: {
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
conditions?: Condition[];
|
||||
}
|
||||
|
||||
interface Condition {
|
||||
operator: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface LogNormal {
|
||||
tau: number;
|
||||
riskAversionParameter: number;
|
||||
params: {
|
||||
mu: number;
|
||||
r: number;
|
||||
sigma: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface PriceMonitoringParameters {
|
||||
triggers: Trigger[];
|
||||
}
|
||||
|
||||
interface Trigger {
|
||||
horizon: string;
|
||||
probability: string;
|
||||
auctionExtension: string;
|
||||
}
|
||||
|
||||
interface Buy {
|
||||
offset: string;
|
||||
proportion: number;
|
||||
reference: string;
|
||||
}
|
||||
|
||||
export interface ProposalSubmission {
|
||||
rationale: {
|
||||
description: string;
|
||||
hash?: string;
|
||||
url?: string;
|
||||
};
|
||||
terms:
|
||||
| ProposalFreeformTerms
|
||||
| ProposalNewMarketTerms
|
||||
| ProposalUpdateMarketTerms
|
||||
| ProposalNetworkParameterTerms
|
||||
| ProposalNewAssetTerms;
|
||||
}
|
||||
|
||||
export interface ProposalSubmissionBody extends BaseTransaction {
|
||||
proposalSubmission: ProposalSubmission;
|
||||
}
|
||||
|
||||
export enum VegaWalletVoteValue {
|
||||
Yes = 'VALUE_YES',
|
||||
No = 'VALUE_NO',
|
||||
@ -111,7 +275,8 @@ export type TransactionSubmission =
|
||||
| VoteSubmissionBody
|
||||
| DelegateSubmissionBody
|
||||
| UndelegateSubmissionBody
|
||||
| OrderAmendmentBody;
|
||||
| OrderAmendmentBody
|
||||
| ProposalSubmissionBody;
|
||||
|
||||
export type TransactionResponse = z.infer<typeof TransactionResponseSchema>;
|
||||
export type GetKeysResponse = z.infer<typeof GetKeysSchema>;
|
||||
@ -120,7 +285,7 @@ export type VegaKey = IterableElement<GetKeysResponse['keys']>;
|
||||
export type TransactionError =
|
||||
| {
|
||||
errors: {
|
||||
'*': string[];
|
||||
[key: string]: string[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
|
@ -23,6 +23,7 @@
|
||||
"@vegaprotocol/deposits": ["libs/deposits/src/index.ts"],
|
||||
"@vegaprotocol/environment": ["libs/environment/src/index.ts"],
|
||||
"@vegaprotocol/fills": ["libs/fills/src/index.ts"],
|
||||
"@vegaprotocol/governance": ["libs/governance/src/index.ts"],
|
||||
"@vegaprotocol/market-depth": ["libs/market-depth/src/index.ts"],
|
||||
"@vegaprotocol/market-list": ["libs/market-list/src/index.ts"],
|
||||
"@vegaprotocol/network-info": ["libs/network-info/src/index.ts"],
|
||||
|
@ -10,6 +10,7 @@
|
||||
"explorer": "apps/explorer",
|
||||
"explorer-e2e": "apps/explorer-e2e",
|
||||
"fills": "libs/fills",
|
||||
"governance": "libs/governance",
|
||||
"market-depth": "libs/market-depth",
|
||||
"market-list": "libs/market-list",
|
||||
"network-info": "libs/network-info",
|
||||
|
Loading…
Reference in New Issue
Block a user