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 {
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>

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.",
"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"
}

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 { 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 /> },
],
},

View File

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

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 {
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[];
};
}
| {

View File

@ -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"],

View File

@ -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",