feat (#804): simple proposal form (#942)

* 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:
Sam Keen 2022-08-05 12:01:46 +01:00 committed by GitHub
parent 4c7ff15f23
commit c40d71e1ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 925 additions and 18 deletions

View File

@ -6,9 +6,9 @@ import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import { InputError } from '@vegaprotocol/ui-toolkit'; import { InputError } from '@vegaprotocol/ui-toolkit';
import { import {
DealTicketAmount, DealTicketAmount,
getDialogTitle, getOrderDialogTitle,
getDialogIntent, getOrderDialogIntent,
getDialogIcon, getOrderDialogIcon,
MarketSelector, MarketSelector,
} from '@vegaprotocol/deal-ticket'; } from '@vegaprotocol/deal-ticket';
import type { Order } from '@vegaprotocol/orders'; import type { Order } from '@vegaprotocol/orders';
@ -143,9 +143,9 @@ export const DealTicketSteps = ({
partyData={partyData} partyData={partyData}
/> />
<TransactionDialog <TransactionDialog
title={getDialogTitle(finalizedOrder?.status)} title={getOrderDialogTitle(finalizedOrder?.status)}
intent={getDialogIntent(finalizedOrder?.status)} intent={getOrderDialogIntent(finalizedOrder?.status)}
icon={getDialogIcon(finalizedOrder?.status)} icon={getOrderDialogIcon(finalizedOrder?.status)}
> >
<OrderFeedback transaction={transaction} order={finalizedOrder} /> <OrderFeedback transaction={transaction} order={finalizedOrder} />
</TransactionDialog> </TransactionDialog>

View File

@ -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.", "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.", "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", "proposals": "Proposals",
"proposal": "Proposal",
"submitting": "Submitting",
"submit": "Submit",
"proposedEnactment": "Proposed enactment", "proposedEnactment": "Proposed enactment",
"Enacted": "Enacted", "Enacted": "Enacted",
"enactedOn": "Enacted on", "enactedOn": "Enacted on",
@ -620,5 +623,7 @@
"InvalidAssetDetails": "Invalid asset details", "InvalidAssetDetails": "Invalid asset details",
"FilterProposals": "Filter proposals", "FilterProposals": "Filter proposals",
"FilterProposalsDescription": "Filter by proposal ID or proposer ID", "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"
} }

View File

@ -0,0 +1 @@
export { Propose } from './propose';

View 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>
</>
);
};

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { ProposalContainer } from './governance/proposal'; import { ProposalContainer } from './governance/proposal';
import { ProposalsContainer } from './governance/proposals'; import { ProposalsContainer } from './governance/proposals';
import { Propose } from './governance/propose';
import Home from './home'; import Home from './home';
import NotFound from './not-found'; import NotFound from './not-found';
@ -165,6 +166,7 @@ const routerConfig = [
component: LazyGovernance, component: LazyGovernance,
children: [ children: [
{ path: ':proposalId', element: <ProposalContainer /> }, { path: ':proposalId', element: <ProposalContainer /> },
{ path: 'propose', element: <Propose /> },
{ index: true, element: <ProposalsContainer /> }, { index: true, element: <ProposalsContainer /> },
], ],
}, },

View File

@ -34,9 +34,9 @@ export const DealTicketManager = ({
/> />
)} )}
<TransactionDialog <TransactionDialog
title={getDialogTitle(finalizedOrder?.status)} title={getOrderDialogTitle(finalizedOrder?.status)}
intent={getDialogIntent(finalizedOrder?.status)} intent={getOrderDialogIntent(finalizedOrder?.status)}
icon={getDialogIcon(finalizedOrder?.status)} icon={getOrderDialogIcon(finalizedOrder?.status)}
> >
<OrderFeedback transaction={transaction} order={finalizedOrder} /> <OrderFeedback transaction={transaction} order={finalizedOrder} />
</TransactionDialog> </TransactionDialog>
@ -44,7 +44,9 @@ export const DealTicketManager = ({
); );
}; };
export const getDialogTitle = (status?: OrderStatus): string | undefined => { export const getOrderDialogTitle = (
status?: OrderStatus
): string | undefined => {
if (!status) { if (!status) {
return; 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) { if (!status) {
return; 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) { if (!status) {
return; return;
} }

12
libs/governance/.babelrc Normal file
View File

@ -0,0 +1,12 @@
{
"presets": [
[
"@nrwl/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}

View 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": {}
}
]
}

View 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).

View 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'],
};

View File

@ -0,0 +1,4 @@
{
"name": "@vegaprotocol/governance",
"version": "0.0.1"
}

View 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
}
}
}
}

View File

@ -0,0 +1,2 @@
export * from './lib';
export * from './utils';

View File

@ -0,0 +1,2 @@
export * from './proposals-hooks';
export * from './proposal-form';

View 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();
});
});

View 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>
);
};

View 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;
}

View File

@ -0,0 +1,3 @@
export * from './__generated__/ProposalEvent';
export * from './use-proposal-event';
export * from './use-proposal-submit';

View File

@ -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;
};

View File

@ -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,
};
};

View File

@ -0,0 +1 @@
import '@testing-library/jest-dom';

View File

@ -0,0 +1 @@
export * from './proposal-dialog-helpers';

View 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;
}
};

View 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"
}
]
}

View 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"]
}

View 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"
]
}

View File

@ -219,10 +219,15 @@ export class RestConnector implements VegaConnector {
} }
if ('errors' in err) { 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( private async request(
@ -240,9 +245,10 @@ export class RestConnector implements VegaConnector {
if (!fetchResult.ok) { if (!fetchResult.ok) {
const errorData = await fetchResult.json(); const errorData = await fetchResult.json();
const error = this.parseError(errorData);
return { return {
status: fetchResult.status, status: fetchResult.status,
error: this.parseError(errorData), error,
}; };
} }

View File

@ -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 { export enum VegaWalletVoteValue {
Yes = 'VALUE_YES', Yes = 'VALUE_YES',
No = 'VALUE_NO', No = 'VALUE_NO',
@ -111,7 +275,8 @@ export type TransactionSubmission =
| VoteSubmissionBody | VoteSubmissionBody
| DelegateSubmissionBody | DelegateSubmissionBody
| UndelegateSubmissionBody | UndelegateSubmissionBody
| OrderAmendmentBody; | OrderAmendmentBody
| ProposalSubmissionBody;
export type TransactionResponse = z.infer<typeof TransactionResponseSchema>; export type TransactionResponse = z.infer<typeof TransactionResponseSchema>;
export type GetKeysResponse = z.infer<typeof GetKeysSchema>; export type GetKeysResponse = z.infer<typeof GetKeysSchema>;
@ -120,7 +285,7 @@ export type VegaKey = IterableElement<GetKeysResponse['keys']>;
export type TransactionError = export type TransactionError =
| { | {
errors: { errors: {
'*': string[]; [key: string]: string[];
}; };
} }
| { | {

View File

@ -23,6 +23,7 @@
"@vegaprotocol/deposits": ["libs/deposits/src/index.ts"], "@vegaprotocol/deposits": ["libs/deposits/src/index.ts"],
"@vegaprotocol/environment": ["libs/environment/src/index.ts"], "@vegaprotocol/environment": ["libs/environment/src/index.ts"],
"@vegaprotocol/fills": ["libs/fills/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-depth": ["libs/market-depth/src/index.ts"],
"@vegaprotocol/market-list": ["libs/market-list/src/index.ts"], "@vegaprotocol/market-list": ["libs/market-list/src/index.ts"],
"@vegaprotocol/network-info": ["libs/network-info/src/index.ts"], "@vegaprotocol/network-info": ["libs/network-info/src/index.ts"],

View File

@ -10,6 +10,7 @@
"explorer": "apps/explorer", "explorer": "apps/explorer",
"explorer-e2e": "apps/explorer-e2e", "explorer-e2e": "apps/explorer-e2e",
"fills": "libs/fills", "fills": "libs/fills",
"governance": "libs/governance",
"market-depth": "libs/market-depth", "market-depth": "libs/market-depth",
"market-list": "libs/market-list", "market-list": "libs/market-list",
"network-info": "libs/network-info", "network-info": "libs/network-info",