Compare commits

...
Author SHA1 Message Date
Matthew Russell eb603ce6ea fix: check for params before rendering 2022-09-13 21:43:36 -07:00
Matthew Russell cc0e4987e5 fix: update net param form, remove old net params obj 2022-09-13 21:25:14 -07:00
Matthew Russell a96bfeec8f refactor: network params to return an object 2022-09-13 20:49:59 -07:00
sam-keen 1c5f44bb6c Feat/800: Using renamed Dialog passed in from useProposalSubmit 2022-09-13 20:44:42 +01:00
sam-keen 9b9cd22b63 Merge remote-tracking branch 'origin/develop' into feat/800-better-proposal-ux-round-1
# Conflicts:
#	apps/token/src/hooks/use-network-param.tsx
#	libs/governance/src/lib/index.ts
#	libs/governance/src/lib/proposal-form.tsx
#	libs/ui-toolkit/src/components/dialog/dialog.tsx
2022-09-13 20:33:37 +01:00
sam-keen 35eb7543a7 Feat/800: Added some extra docs links 2022-09-13 20:29:46 +01:00
sam-keen 1ef017e14c Feat/800: Fixed type issues that were preventing spotting an error 2022-09-13 19:20:16 +01:00
mattrussell36 2eea3e2e34 chore: update tranches
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-09-13 18:05:16 +00:00
sam-keen a989d3a0b2 Feat/800: Corrected mistake in get-enactment-timestamp.ts 2022-09-13 18:05:55 +01:00
sam-keen 4f8a585956 Feat/800: Improved tests for the proposal forms 2022-09-13 16:58:07 +01:00
sam-keen e67f322400 Feat/800: Tests for the other reusable form components 2022-09-13 16:57:32 +01:00
sam-keen 2a700ddd84 Feat/800: Improved workings of proposal-form-min-requirements.tsx and wrote tests 2022-09-13 16:57:09 +01:00
sam-keen 238e019708 Feat/800: Added validation options to proposal-form-vote-and-enactment-deadline.tsx, generally improved the component and added tests 2022-09-13 16:56:23 +01:00
sam-keen f701124f0f Feat/800: More translations 2022-09-13 16:55:15 +01:00
sam-keen c5613f0107 Feat/800: Timestamp functions for closing, enactment and validation, with tests 2022-09-13 16:54:51 +01:00
AndyWhiteVega 040cf0b83b Test/governance suite expansion (#1317)
* test: initial commit

* test: commit

* test: extra tests and references added also new tag 0.55.0 for builds

* test: lint

* test: back to 0.54 for capsule cypress until tests fixed

* test: skip two tests since wallet name no longer shown soon to be fixed
2022-09-13 13:28:48 +01:00
mattrussell36 9d2f2fe3aa chore: update tranches
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-09-13 12:05:22 +00:00
Bartłomiej Głowniaandasiaznik a08f63c7d8 Feature/1243 market data providers refactoring (#1254)
* feat(#1243): Market data providers

* feat(#1243): Market data providers

* feat(#1243): Market data providers

* feat(#1243): refactor market lists

* feat(#1243): refactor market lists

* feat(#1243): refactor market lists

* feat(#1243): refactor orderbook data providers

* feat(#1243): refactor orderbook data providers

* feat(#1243): refactor depth chart data layer

* feat(#1243): fix market depth typing

* fixed unit tests

* feat(#1243): e2e test fixes

* feat(#1243): code style fixes

* feat(#1243): post merge fixes

* feat(#1243): fix lint issues

* feat(#1243): post merge fixes

* feat(#1243): remove market name from market

* feat(#1243): fix console lite e2e data depth mocks

* feat(#1243): fix console lite e2e market trade test

* feat(#1243): fix lint issues

* feat(#1243): fix trading e2e home test

Co-authored-by: asiaznik <artur@vegaprotocol.io>
2022-09-13 12:14:06 +02:00
Matthew Russell 3ff5bbb5a7 chore: use trading header and tidy tailwind usage (#1313)
* fix: border too thick, use grid head component

* fix: unused and superfluous classes, define border color in one place

* fix: tooltip should be on data not label
2022-09-13 09:19:41 +01:00
AndyWhiteVega 535bdb5183 Test/governance and wallet tweaks (#1309)
* test: tweaks

* test: lint

* test: lint

* test: remove unnessecary timeouts

* test: lint

* test: syntax tweak
2022-09-13 08:43:40 +01:00
mattrussell36 4505e86363 chore: update tranches
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-09-13 06:26:28 +00:00
mattrussell36 7b2e86f32e chore: update tranches
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-09-13 00:12:14 +00:00
sam-keen a6ae5dd526 Feat/800: Corrected network param form network params 2022-09-08 17:44:33 +01:00
sam-keen 6d5aaa29ec Feat/800: For network params that aren't JSON, display the network param current value in a readonly text input rather than the syntax highlighter 2022-09-08 15:20:19 +01:00
sam-keen 9ff908cf4d Feat/800: Set a default select option for the market selector 2022-09-08 15:19:33 +01:00
sam-keen bccfa05c89 Feat/800: Condense vote and enactment ui into single component for simpler state management, memoised some stuff 2022-09-08 15:03:29 +01:00
sam-keen 01cb4fe74e Feat/800: Removed wallet-confirmation buffer, suspect it is not necessary 2022-09-08 15:02:28 +01:00
sam-keen 0ccb70b68b Feat/800: Show current value of selected network param in proposal form 2022-09-07 16:50:48 +01:00
sam-keen 55318f18bc Feat/800: Added useful explorer links to proposal forms 2022-09-07 13:02:43 +01:00
sam-keen 22c2e49a98 Feat/800: Improved way to pass docs url 2022-09-07 12:35:49 +01:00
sam-keen 48110180f2 Feat/800: Network param value change now a textarea 2022-09-07 12:17:15 +01:00
sam-keen bd3c27b9dc Feat/800: Added the spam protection min tokens network param to proposal forms requests, and the min requirements now displays the larger value of spam protection or min proposer balance 2022-09-07 12:15:22 +01:00
sam-keen 5a1b5b0efe Feat/800: Another type error from API changes fixed 2022-09-07 11:51:18 +01:00
sam-keen c5fb26bd4c Feat/800: Deadlines to the bottom of proposal forms 2022-09-07 11:08:15 +01:00
sam-keen cb455e2399 Feat/800: Set up env-specific proposal docs links 2022-09-07 11:00:26 +01:00
sam-keen 81f11fbb6b Feat/800: Updating simple proposal form tests 2022-09-07 10:59:46 +01:00
sam-keen aaecccc386 Feat/800: Fixing small types error in test 2022-09-07 10:58:29 +01:00
sam-keen e3a0b9a3da Feat/800: Some basic proposal form tests 2022-09-06 13:01:10 +01:00
sam-keen 7841c06121 Feat/800: Raw proposal test (tests as per old proposal form test) 2022-09-06 12:53:59 +01:00
sam-keen 15dfbee0d3 Feat/800: Another translation addition 2022-09-06 12:53:20 +01:00
sam-keen d2637a9645 Feat/800: Couple of translation additions 2022-09-05 23:21:38 +01:00
sam-keen 7695ea567a Merge remote-tracking branch 'origin/master' into feat/800-better-proposal-ux-round-1
# Conflicts:
#	libs/ui-toolkit/src/components/dialog/dialog.tsx
#	libs/ui-toolkit/src/components/form-group/form-group.tsx
2022-09-05 23:00:30 +01:00
sam-keen a8a3753237 Feat/800: Reworked the proposal forms 2022-09-05 22:57:19 +01:00
sam-keen 8cc499880c Feat/800: Added iso8601-duration package for working with API vote deadline and enactment data 2022-09-05 22:56:52 +01:00
sam-keen aedc4ba223 Feat/800: Regenerated types for network params 2022-09-05 22:55:53 +01:00
sam-keen 1579439730 Feat/800: Tweak to icon positioning on the dialog component 2022-09-05 22:53:45 +01:00
sam-keen 8416d9a15e Feat/800: Readded necessary 'color-scheme-dark' class for dark-theme date and datetime inputs 2022-09-05 22:52:58 +01:00
sam-keen aa34dc6751 Feat/800: Added utils for vote deadline and enactment timestamps 2022-09-05 22:51:12 +01:00
sam-keen c39c111a1c Feat/800: Removed old catchall file of reusable proposal form components 2022-09-05 22:50:04 +01:00
sam-keen 2cdfb63050 Feat/800: Updated and built more reusable proposal form components 2022-09-05 22:49:12 +01:00
sam-keen 52b1c24070 Feat/800: Updated network params, added new method to get param keys as well as values, updated generated types 2022-09-05 22:48:04 +01:00
sam-keen 3a0a527805 Feat/927: Proposal forms built with reusable components 2022-09-01 17:20:25 +01:00
Dexter b174d275c7 chore: fix typings for shared components 2022-09-01 10:08:02 +01:00
sam-keen dc32655e02 Feat/927: Hydrating the new proposal forms 2022-09-01 09:53:27 +01:00
sam-keen 7d4ac4648a Merge remote-tracking branch 'origin/master' into feat/800-better-proposal-ux-round-1
# Conflicts:
#	apps/token/src/config/links.ts
2022-08-31 10:43:52 +01:00
sam-keen d314b20a08 Feat/800: Link style tweak for proposals guide 2022-08-28 21:26:43 +01:00
sam-keen 5925dc006f Feat/800: Refactored propose.tsx to be a hub for the new proposal routes 2022-08-26 18:30:59 +01:00
sam-keen b05da4dceb Feat/800: Configured routes for different proposal types 2022-08-26 16:57:24 +01:00
160 changed files with 6155 additions and 2044 deletions
@@ -78,7 +78,7 @@ jobs:
uses: actions/checkout@v2
with:
repository: vegaprotocol/vega
ref: v0.54.0
ref: v0.55.0
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
path: './vega'
@@ -62,7 +62,7 @@ jobs:
uses: actions/checkout@v2
with:
repository: vegaprotocol/vega
ref: v0.54.0
ref: v0.55.0
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
path: './vega'
@@ -9,6 +9,7 @@ import { generatePartyBalance } from '../support/mocks/generate-party-balance';
import { generatePartyMarketData } from '../support/mocks/generate-party-market-data';
import { generateMarketMarkPrice } from '../support/mocks/generate-market-mark-price';
import { generateMarketNames } from '../support/mocks/generate-market-names';
import { generateMarketDepth } from '../support/mocks/generate-market-depth';
describe('market selector', () => {
let markets;
@@ -23,6 +24,7 @@ describe('market selector', () => {
aliasQuery(req, 'PartyMarketData', generatePartyMarketData());
aliasQuery(req, 'MarketMarkPrice', generateMarketMarkPrice());
aliasQuery(req, 'MarketNames', generateMarketNames());
aliasQuery(req, 'MarketDepth', generateMarketDepth());
});
cy.visit('/markets');
@@ -1,5 +1,8 @@
import { aliasQuery } from '@vegaprotocol/cypress';
import { generateSimpleMarkets } from '../support/mocks/generate-markets';
import {
generateSimpleMarkets,
generateMarkets,
} from '../support/mocks/generate-markets';
import { generateDealTicket } from '../support/mocks/generate-deal-ticket';
import { generateMarketTags } from '../support/mocks/generate-market-tags';
import { generateMarketPositions } from '../support/mocks/generate-market-positions';
@@ -14,6 +17,7 @@ describe('Market trade', () => {
let markets;
beforeEach(() => {
cy.mockGQL((req) => {
aliasQuery(req, 'Markets', generateMarkets());
aliasQuery(req, 'SimpleMarkets', generateSimpleMarkets());
aliasQuery(req, 'DealTicketQuery', generateDealTicket());
aliasQuery(req, 'MarketTags', generateMarketTags());
@@ -1,24 +1,15 @@
export const generateMarketDepth = () => {
return {
import merge from 'lodash/merge';
import type { PartialDeep } from 'type-fest';
// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries
import type { MarketDepth } from '../../../../../libs/market-depth/src/lib/__generated__/MarketDepth';
export const generateMarketDepth = (
override?: PartialDeep<MarketDepth>
): MarketDepth => {
const defaultResult: MarketDepth = {
market: {
id: 'a46bd7e5277087723b7ab835844dec3cef8b4445738101269624bf5537d5d423',
decimalPlaces: 5,
positionDecimalPlaces: 0,
data: {
staticMidPrice: '9893006',
marketTradingMode: 'TRADING_MODE_CONTINUOUS',
indicativeVolume: '0',
indicativePrice: '0',
bestStaticBidPrice: '9893006',
bestStaticOfferPrice: '9893006',
market: {
id: 'a46bd7e5277087723b7ab835844dec3cef8b4445738101269624bf5537d5d423',
__typename: 'Market',
},
__typename: 'MarketData',
},
depth: {
lastTrade: { price: '9893006', __typename: 'Trade' },
sell: [
{
price: '9893007',
@@ -215,4 +206,5 @@ export const generateMarketDepth = () => {
__typename: 'Market',
},
};
return merge(defaultResult, override);
};
@@ -1,3 +1,5 @@
import merge from 'lodash/merge';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import { protoMarket } from './commons';
export const generateSimpleMarkets = () => {
@@ -880,3 +882,41 @@ export const generateLongListMarkets = (count: number) => {
}
return { markets };
};
export const generateMarkets = (override?) => {
const markets = [
{
...protoMarket,
decimalPlaces: 5,
positionDecimalPlaces: 0,
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
state: MarketState.STATE_ACTIVE,
marketTimestamps: {
__typename: 'MarketTimestamps',
close: '',
open: '',
},
fees: {
__typename: 'Fees',
factors: {
__typename: 'FeeFactors',
makerFee: '',
infrastructureFee: '',
liquidityFee: '',
},
},
},
];
const defaultResult = {
marketsConnection: {
__typename: 'MarketConnection',
edges: markets.map((node) => ({
__typename: 'MarketEdge',
node,
})),
},
};
return merge(defaultResult, override);
};
@@ -64,6 +64,15 @@ jest.mock('@vegaprotocol/market-depth', () => ({
useOrderBookData: jest.fn(() => mockOrderBookData),
}));
jest.mock('@vegaprotocol/react-helpers', () => ({
...jest.requireActual('@vegaprotocol/react-helpers'),
useDataProvider: jest.fn(() => ({
data: {
marketsConnection: [],
},
})),
}));
describe('useCalculateSlippage Hook', () => {
describe('calculate proper result', () => {
afterEach(() => {
@@ -1,9 +1,15 @@
import { useMemo } from 'react';
import { Side } from '@vegaprotocol/types';
import { useOrderBookData } from '@vegaprotocol/market-depth';
import { marketProvider } from '@vegaprotocol/market-list';
import type { Market } from '@vegaprotocol/market-list';
import type { Order } from '@vegaprotocol/orders';
import { BigNumber } from 'bignumber.js';
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
import {
formatNumber,
toBigNum,
useDataProvider,
} from '@vegaprotocol/react-helpers';
interface Props {
marketId: string;
@@ -16,11 +22,16 @@ const useCalculateSlippage = ({ marketId, order }: Props) => {
variables,
throttleMilliseconds: 5000,
});
const { data: market } = useDataProvider<Market, never>({
dataProvider: marketProvider,
noUpdate: true,
variables,
});
const volPriceArr =
data?.depth[order.side === Side.SIDE_BUY ? 'sell' : 'buy'] || [];
if (volPriceArr.length) {
const decimals = data?.decimalPlaces ?? 0;
const positionDecimals = data?.positionDecimalPlaces ?? 0;
if (volPriceArr.length && market) {
const decimals = market.decimalPlaces ?? 0;
const positionDecimals = market.positionDecimalPlaces ?? 0;
const bestPrice = toBigNum(volPriceArr[0].price, decimals);
const { size } = order;
let descSize = new BigNumber(size);
@@ -10,6 +10,7 @@ import {
formatNumber,
t,
} from '@vegaprotocol/react-helpers';
import { isJsonObject } from '@vegaprotocol/react-helpers';
import { RouteTitle } from '../../components/route-title';
import type {
NetworkParametersQuery,
@@ -85,14 +86,6 @@ export const NetworkParameterRow = ({
);
};
export const isJsonObject = (str: string) => {
try {
return JSON.parse(str) && Object.keys(JSON.parse(str)).length > 0;
} catch (e) {
return false;
}
};
export const NETWORK_PARAMETERS_QUERY = gql`
query NetworkParametersQuery {
networkParameters {
+97 -22
View File
@@ -232,7 +232,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "129999.45",
"total_removed": "0",
"locked_amount": "106335.413484289455510975",
"locked_amount": "106098.1360429719255366",
"deposits": [
{
"amount": "129999.45",
@@ -298,7 +298,7 @@
"tranche_end": "2023-09-03T00:00:00.000Z",
"total_added": "52600",
"total_removed": "0",
"locked_amount": "51194.43613013698976",
"locked_amount": "51050.29485032978276",
"deposits": [
{
"amount": "2600",
@@ -682,7 +682,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "97499.58",
"total_removed": "0",
"locked_amount": "47527.3366591736311428252",
"locked_amount": "47294.5895926345761588126",
"deposits": [
{
"amount": "97499.58",
@@ -715,7 +715,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "135173.4239508",
"total_removed": "0",
"locked_amount": "64961.663960720168133509560356",
"locked_amount": "64643.53890286602609186297228",
"deposits": [
{
"amount": "135173.4239508",
@@ -748,7 +748,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "32499.86",
"total_removed": "0",
"locked_amount": "19993.929779310993153618",
"locked_amount": "19896.017107744868930832",
"deposits": [
{
"amount": "32499.86",
@@ -781,7 +781,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "10833.29",
"total_removed": "0",
"locked_amount": "6507.830126663603897055",
"locked_amount": "6475.960502190905203881",
"deposits": [
{
"amount": "10833.29",
@@ -889,7 +889,7 @@
"tranche_end": "2022-11-01T00:00:00.000Z",
"total_added": "30000",
"total_removed": "0",
"locked_amount": "8029.330842391305",
"locked_amount": "7866.2515096618368",
"deposits": [
{
"amount": "7500",
@@ -1006,7 +1006,7 @@
"tranche_end": "2023-06-02T00:00:00.000Z",
"total_added": "1939928.38",
"total_removed": "479504.6187210477481",
"locked_amount": "1393806.983975873425857912",
"locked_amount": "1388490.942922118064795974",
"deposits": [
{
"amount": "1852091.69",
@@ -1292,7 +1292,7 @@
"tranche_end": "2023-02-01T00:00:00.000Z",
"total_added": "42500",
"total_removed": "0",
"locked_amount": "32624.88536005434875",
"locked_amount": "32393.8563053542705",
"deposits": [
{
"amount": "12500",
@@ -2159,7 +2159,7 @@
"tranche_end": "2022-09-30T00:00:00.000Z",
"total_added": "60916.66666633337",
"total_removed": "20696.25757434884556896",
"locked_amount": "2693.8540998784246905956196508555",
"locked_amount": "2537.6231698974791687009338450295",
"deposits": [
{
"amount": "2833.333333",
@@ -5892,7 +5892,7 @@
"tranche_id": 11,
"tranche_start": "2021-09-03T00:00:00.000Z",
"tranche_end": "2022-09-03T00:00:00.000Z",
"total_added": "35958.000000000000000003",
"total_added": "35972.000000000000000003",
"total_removed": "17098.49537348884",
"locked_amount": "0",
"deposits": [
@@ -5911,6 +5911,16 @@
"user": "0xEED071df0b6f8C4088b9CfD9f2ADE131C8591613",
"tx": "0x0d569e9b901099d43f017945635ad8c2b566ce2290abb2583b018e83df34642b"
},
{
"amount": "2",
"user": "0x60901F5a6a66BCaD910A97e85373A00996B361f5",
"tx": "0xa87cd2caf4f27d7a60443ec952b831b005a809803fc899c4bcc3ca12016299eb"
},
{
"amount": "12",
"user": "0x60901F5a6a66BCaD910A97e85373A00996B361f5",
"tx": "0x056bbadf095fbbee4dc0ccac4431c998c65ad3e714a76844c71bdb0509b0ced5"
},
{
"amount": "15",
"user": "0xb4eE687f019A8e48F7087f4b4f8653208B8cc48f",
@@ -12530,6 +12540,27 @@
"withdrawn_tokens": "0",
"remaining_tokens": "2"
},
{
"address": "0x60901F5a6a66BCaD910A97e85373A00996B361f5",
"deposits": [
{
"amount": "2",
"user": "0x60901F5a6a66BCaD910A97e85373A00996B361f5",
"tranche_id": 11,
"tx": "0xa87cd2caf4f27d7a60443ec952b831b005a809803fc899c4bcc3ca12016299eb"
},
{
"amount": "12",
"user": "0x60901F5a6a66BCaD910A97e85373A00996B361f5",
"tranche_id": 11,
"tx": "0x056bbadf095fbbee4dc0ccac4431c998c65ad3e714a76844c71bdb0509b0ced5"
}
],
"withdrawals": [],
"total_tokens": "14",
"withdrawn_tokens": "0",
"remaining_tokens": "14"
},
{
"address": "0xb4eE687f019A8e48F7087f4b4f8653208B8cc48f",
"deposits": [
@@ -22455,7 +22486,7 @@
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "3732368.4671",
"total_removed": "204390.7646836173642",
"locked_amount": "2166297.38704960471950391605",
"locked_amount": "2158128.48279889755415607996",
"deposits": [
{
"amount": "1998.95815",
@@ -23322,8 +23353,8 @@
"tranche_start": "2022-06-05T00:00:00.000Z",
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "15870102.715470999700000001",
"total_removed": "203050.3452740934272444",
"locked_amount": "12981239.0305324649112059509051105297445255",
"total_removed": "203595.9963255265343694",
"locked_amount": "12952272.620553243128136100283526560935388",
"deposits": [
{
"amount": "16249.93",
@@ -23852,6 +23883,16 @@
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xe0040011ab92c3d951d3449fa6159e095b1893639c47fdb1a72aa87c741bdf56"
},
{
"amount": "208.439735314624",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xa2cf8d5f7caa97b4c6804c0bce8003115d61772edf6ab6f8c9b3de503eafd2f5"
},
{
"amount": "337.211316118483125",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0x1e6bd91941381b74b4bec158b2366838830987396925340281bce36d0a75a514"
},
{
"amount": "305.8599694076328",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -24435,6 +24476,18 @@
"tranche_id": 2,
"tx": "0xe0040011ab92c3d951d3449fa6159e095b1893639c47fdb1a72aa87c741bdf56"
},
{
"amount": "208.439735314624",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 2,
"tx": "0xa2cf8d5f7caa97b4c6804c0bce8003115d61772edf6ab6f8c9b3de503eafd2f5"
},
{
"amount": "337.211316118483125",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 2,
"tx": "0x1e6bd91941381b74b4bec158b2366838830987396925340281bce36d0a75a514"
},
{
"amount": "305.8599694076328",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -24893,8 +24946,8 @@
}
],
"total_tokens": "259998.8875",
"withdrawn_tokens": "47135.599794757122",
"remaining_tokens": "212863.287705242878"
"withdrawn_tokens": "47681.250846190229125",
"remaining_tokens": "212317.636653809770875"
},
{
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
@@ -26439,8 +26492,8 @@
"tranche_start": "2021-11-05T00:00:00.000Z",
"tranche_end": "2023-05-05T00:00:00.000Z",
"total_added": "14597706.0446472999",
"total_removed": "2764683.361107610704396897",
"locked_amount": "6262751.76070348203965915005868418",
"total_removed": "2765441.081300436948788147",
"locked_amount": "6236010.1559629377107703982051563",
"deposits": [
{
"amount": "129284.449",
@@ -26674,6 +26727,16 @@
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0x5940c1d1266c4aee837eb2a10ea1a42f52182c5fb2d9f4ca10752ec099ddc239"
},
{
"amount": "288.65748698602475075",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0x0fb5472a41850ad312926ca6a39df1901579d075b0b8836459e2cd7f96353a8b"
},
{
"amount": "469.0627058402196405",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0x6e6546452d57d7d58c5fd0671b8a43c2c6024bf845d085fc3133c6851ab18620"
},
{
"amount": "551.363354573845008",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
@@ -28775,6 +28838,18 @@
"tranche_id": 3,
"tx": "0x5940c1d1266c4aee837eb2a10ea1a42f52182c5fb2d9f4ca10752ec099ddc239"
},
{
"amount": "288.65748698602475075",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0x0fb5472a41850ad312926ca6a39df1901579d075b0b8836459e2cd7f96353a8b"
},
{
"amount": "469.0627058402196405",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0x6e6546452d57d7d58c5fd0671b8a43c2c6024bf845d085fc3133c6851ab18620"
},
{
"amount": "551.363354573845008",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
@@ -30511,8 +30586,8 @@
}
],
"total_tokens": "359123.469575",
"withdrawn_tokens": "204783.7136307165325265",
"remaining_tokens": "154339.7559442834674735"
"withdrawn_tokens": "205541.43382354277691775",
"remaining_tokens": "153582.03575145722308225"
},
{
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
@@ -31600,7 +31675,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "5778205.3912159303",
"total_removed": "1773434.744647395163801354",
"locked_amount": "2157547.69392106277665067936509808",
"locked_amount": "2146981.92415619473594330314378902",
"deposits": [
{
"amount": "552496.6455",
@@ -33053,7 +33128,7 @@
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "472355.6199999996",
"total_removed": "3216.01601557326",
"locked_amount": "343262.2040617976027593156164384",
"locked_amount": "341967.79448782570701567102181632",
"deposits": [
{
"amount": "3000",
+1 -1
View File
@@ -89,7 +89,7 @@
"tranche_end": "2022-10-12T00:53:20.000Z",
"total_added": "1010.000000000000000001",
"total_removed": "668.4622323651",
"locked_amount": "81.0314903602232870000802291983764587",
"locked_amount": "78.26388666920349680007748899670218168",
"deposits": [
{
"amount": "1000",
@@ -1,6 +1,6 @@
{
"rationale": {
"title": "An example freeform proposal",
"title": "An example freeform proposal with unique number of: ",
"description": "I propose that everyone evaluate the following IPFS document and vote Yes if they agree. [bafybeigwwctpv37xdcwacqxvekr6e4kaemqsrv34em6glkbiceo3fcy4si](https://dweb.link/ipfs/bafybeigwwctpv37xdcwacqxvekr6e4kaemqsrv34em6glkbiceo3fcy4si)"
},
"terms": {
@@ -23,6 +23,8 @@ const proposalVoteProgressForTokens =
const proposalVoteProgressAgainstTokens =
'[data-testid="vote-progress-indicator-tokens-against"]';
const changeVoteButton = '[data-testid="change-vote-button"]';
const proposalDetailsTitle = '[data-testid="proposal-title"]';
const proposalDetailsDescription = '[data-testid="proposal-description"]';
const voteButtons = '[data-testid="vote-buttons"]';
const rejectProposalsLink = '[href="/governance/rejected"]';
const feedbackError = '[data-testid="Error"]';
@@ -170,7 +172,7 @@ context('Governance flow - with eth and vega wallets connected', function () {
cy.get(dialogCloseButton).click();
});
it('Newly created freeform proposal - able to filter by proposalID to show it in list', function () {
it('Newly created freeform proposals list - able to filter by proposalID to show it in list', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
@@ -196,15 +198,11 @@ context('Governance flow - with eth and vega wallets connected', function () {
.then((proposalId) => {
cy.get('[data-testid="set-proposals-filter-visible"]').click();
cy.get('[data-testid="filter-input"]').type(proposalId);
cy.get(`#${proposalId}`).should(
'contain',
`Freeform proposal: ${proposalId}`,
txTimeout
);
cy.get(`#${proposalId}`).should('contain', proposalId);
});
});
it('Newly created freeform proposal - able to filter by proposerID to show it in list', function () {
it('Newly created freeform proposals list - able to filter by proposerID to show it in list', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
@@ -230,15 +228,57 @@ context('Governance flow - with eth and vega wallets connected', function () {
let proposalId = proposal.response.body.data.busEvents[0].event.id;
cy.get('[data-testid="set-proposals-filter-visible"]').click();
cy.get('[data-testid="filter-input"]').type(proposerId);
cy.get(`#${proposalId}`).should(
'contain',
`Freeform proposal: ${proposalId}`,
txTimeout
);
cy.get(`#${proposalId}`).should('contain', proposalId);
});
});
it('Newly created freeform proposal - shows in an open state', function () {
it('Newly created freeform proposals list - shows title and portion of summary', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
cy.navigate_to('governance');
cy.wait_for_spinner();
cy.get(newProposalButton).should('be.visible').click();
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.enter_unique_freeform_proposal_body(closingDateTimestamp).as(
'freeformProposal'
);
}
);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Awaiting network confirmation', epochTimeout).should(
'be.visible'
);
cy.contains('Proposal submitted', epochTimeout).should('be.visible');
cy.get(dialogCloseButton).click();
cy.wait_for_proposal_sync();
cy.navigate_to('governance');
cy.wait_for_spinner();
cy.wait('@proposalSubmissionCompletion')
.its(proposalResponseProposalIdPath)
.then((proposalId) => {
cy.get(openProposals).within(() => {
cy.get('@freeformProposal').then((freeformProposal) => {
// 1004-VOTE-008
// 1004-VOTE-034
cy.get(`#${proposalId}`)
.should('contain', freeformProposal.rationale.title)
.and('be.visible');
cy.get(`#${proposalId}`)
.should(
'contain',
freeformProposal.rationale.description.substring(0, 59)
)
.and('be.visible');
});
});
});
});
it('Newly created freeform proposals list - shows open proposals in an open state', function () {
// 1004-VOTE-004
// 1004-VOTE-035
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
@@ -263,13 +303,131 @@ context('Governance flow - with eth and vega wallets connected', function () {
.its(proposalResponseProposalIdPath)
.then((proposalId) => {
cy.get(openProposals).within(() => {
// 1004-VOTE-035
cy.get(`#${proposalId}`)
.should('contain', `Freeform proposal: ${proposalId}`, txTimeout)
.should('contain', proposalId)
.and('contain', 'Open')
.and('be.visible')
.within(() => {
cy.get(viewProposalButton).should('be.visible').click();
});
.and('be.visible');
});
});
});
it('Newly created freeform proposal details - shows proposal title and full description', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
cy.navigate_to('governance');
cy.wait_for_spinner();
cy.get(newProposalButton).should('be.visible').click();
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.enter_unique_freeform_proposal_body(closingDateTimestamp).as(
'freeformProposal'
);
}
);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Awaiting network confirmation', epochTimeout).should(
'be.visible'
);
cy.contains('Proposal submitted', epochTimeout).should('be.visible');
cy.get(dialogCloseButton).click();
cy.wait_for_proposal_sync();
cy.navigate_to('governance');
cy.wait_for_spinner();
cy.wait('@proposalSubmissionCompletion')
.its(proposalResponseProposalIdPath)
.then((proposalId) => {
cy.get(openProposals).within(() => {
cy.get(`#${proposalId}`).within(() => {
cy.get(viewProposalButton).should('be.visible').click();
});
});
cy.get('@freeformProposal').then((freeformProposal) => {
// 1004-VOTE-054
cy.get(proposalDetailsTitle)
.should('contain', freeformProposal.rationale.title)
.and('be.visible');
cy.get(proposalDetailsDescription)
.should('contain', freeformProposal.rationale.description)
.and('be.visible');
});
});
});
it('Newly created freeform proposal details - shows full link included in description', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
cy.navigate_to('governance');
cy.wait_for_spinner();
cy.get(newProposalButton).should('be.visible').click();
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.enter_unique_freeform_proposal_body(closingDateTimestamp).as(
'freeformProposal'
);
}
);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Awaiting network confirmation', epochTimeout).should(
'be.visible'
);
cy.contains('Proposal submitted', epochTimeout).should('be.visible');
cy.get(dialogCloseButton).click();
cy.wait_for_proposal_sync();
cy.navigate_to('governance');
cy.wait_for_spinner();
cy.wait('@proposalSubmissionCompletion')
.its(proposalResponseProposalIdPath)
.then((proposalId) => {
cy.get(openProposals).within(() => {
cy.get(`#${proposalId}`).within(() => {
cy.get(viewProposalButton).should('be.visible').click();
});
});
cy.get('@freeformProposal').then((freeformProposal) => {
// 1004-VOTE-055
cy.get(proposalDetailsDescription)
.should('contain', freeformProposal.rationale.description)
.and('have.attr', 'href')
.and(
'equal',
'https://dweb.link/ipfs/bafybeigwwctpv37xdcwacqxvekr6e4kaemqsrv34em6glkbiceo3fcy4si'
)
.and('be.visible');
});
});
});
it('Newly created freeform proposal details - shows open proposal in an open state', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
cy.navigate_to('governance');
cy.wait_for_spinner();
cy.get(newProposalButton).should('be.visible').click();
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.enter_unique_freeform_proposal_body(closingDateTimestamp);
}
);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Awaiting network confirmation', epochTimeout).should(
'be.visible'
);
cy.contains('Proposal submitted', epochTimeout).should('be.visible');
cy.get(dialogCloseButton).click();
cy.wait_for_proposal_sync();
cy.navigate_to('governance');
cy.wait_for_spinner();
cy.wait('@proposalSubmissionCompletion')
.its(proposalResponseProposalIdPath)
.then((proposalId) => {
cy.get(openProposals).within(() => {
cy.get(`#${proposalId}`).within(() => {
cy.get(viewProposalButton).should('be.visible').click();
});
});
cy.get_proposal_information_from_table('ID')
.contains(proposalId)
@@ -283,7 +441,7 @@ context('Governance flow - with eth and vega wallets connected', function () {
});
});
it('Newly created freeform proposal - shows proposed and closing dates', function () {
it('Newly created freeform proposal details - shows proposed and closing dates', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
@@ -322,10 +480,14 @@ context('Governance flow - with eth and vega wallets connected', function () {
.should('be.visible');
}
);
// 1004-VOTE-043
cy.contains('9 days left to vote').should('be.visible');
});
it('Newly created freeform proposal - shows default status set to fail', function () {
it('Newly created freeform proposal details - shows default status set to fail', function () {
// 1004-VOTE-037
// 1004-VOTE-039
// 1004-VOTE-040
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
@@ -356,6 +518,7 @@ context('Governance flow - with eth and vega wallets connected', function () {
cy.get_proposal_information_from_table('Will pass')
.contains('👎')
.should('be.visible');
// 1004-VOTE-062
cy.get_proposal_information_from_table('Majority met')
.contains('👎')
.should('be.visible');
@@ -364,7 +527,7 @@ context('Governance flow - with eth and vega wallets connected', function () {
.should('be.visible');
});
it('Newly created freeform proposal - ability to vote for proposal - with minimum required tokens associated', function () {
it('Newly created freeform proposal details - ability to vote for proposal - with minimum required tokens associated', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
@@ -393,6 +556,8 @@ context('Governance flow - with eth and vega wallets connected', function () {
'0',
'shortMonth'
).then((votedDate) => {
// 1004-VOTE-051
// 1004-VOTE-093
cy.contains('You voted:')
.siblings()
.contains('For')
@@ -416,9 +581,11 @@ context('Governance flow - with eth and vega wallets connected', function () {
cy.get_proposal_information_from_table('Tokens against proposal')
.should('have.text', '0.00')
.and('be.visible');
// 1004-VOTE-061
cy.get_proposal_information_from_table('Participation required')
.contains(`${this.requiredParticipation}%`)
.should('be.visible');
// 1004-VOTE-066
cy.get_proposal_information_from_table('Majority Required')
.contains(`${parseFloat(this.requiredMajority).toFixed(2)}%`)
.should('be.visible');
@@ -427,7 +594,7 @@ context('Governance flow - with eth and vega wallets connected', function () {
.and('be.visible');
});
it('Newly created freeform proposal - ability to vote against proposal - with minimum required tokens associated', function () {
it('Newly created freeform proposal details - ability to vote against proposal - with minimum required tokens associated', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
@@ -456,6 +623,7 @@ context('Governance flow - with eth and vega wallets connected', function () {
'0',
'shortMonth'
).then((votedDate) => {
// 1004-VOTE-051
cy.contains('You voted:')
.siblings()
.contains('Against')
@@ -490,7 +658,7 @@ context('Governance flow - with eth and vega wallets connected', function () {
.and('be.visible');
});
it('Newly created freeform proposal - ability to change vote from against to for - with minimum required tokens associated', function () {
it('Newly created freeform proposal details - ability to change vote from against to for - with minimum required tokens associated', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
@@ -515,6 +683,7 @@ context('Governance flow - with eth and vega wallets connected', function () {
.as('submittedProposal')
.within(() => cy.get(viewProposalButton).click());
cy.vote_for_proposal('against');
// 1004-VOTE-090
cy.get(changeVoteButton).should('be.visible').click();
cy.wait_for_spinner();
cy.vote_for_proposal('for');
@@ -528,6 +697,7 @@ context('Governance flow - with eth and vega wallets connected', function () {
cy.get(proposalVoteProgressAgainstTokens)
.contains('0.00')
.and('be.visible');
// 1004-VOTE-064
cy.get_proposal_information_from_table('Tokens for proposal')
.should('have.text', parseFloat(this.minProposerBalance).toFixed(2))
.and('be.visible');
@@ -545,7 +715,7 @@ context('Governance flow - with eth and vega wallets connected', function () {
.and('be.visible');
});
it('Newly created freeform proposal - ability to change vote from for to against - with minimum required tokens associated', function () {
it('Newly created freeform proposal details - ability to change vote from for to against - with minimum required tokens associated', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
@@ -572,6 +742,7 @@ context('Governance flow - with eth and vega wallets connected', function () {
cy.vote_for_proposal('for');
cy.get(changeVoteButton).should('be.visible').click();
cy.wait_for_spinner();
// 1004-VOTE-080
cy.vote_for_proposal('against');
cy.get(proposalVoteProgressForPercentage)
.contains('0.00%')
@@ -600,7 +771,7 @@ context('Governance flow - with eth and vega wallets connected', function () {
.and('be.visible');
});
it('Newly created freeform proposal - ability to increase associated tokens - so that vote sways result', function () {
it('Newly created freeform proposal details - ability to increase associated tokens - so that vote sways result', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
@@ -624,6 +795,7 @@ context('Governance flow - with eth and vega wallets connected', function () {
cy.get_submitted_proposal_from_proposal_list()
.as('submittedProposal')
.within(() => cy.get(viewProposalButton).click());
// 1004-VOTE-080
cy.vote_for_proposal('for');
cy.get_proposal_information_from_table('Total Supply')
.invoke('text')
@@ -645,6 +817,8 @@ context('Governance flow - with eth and vega wallets connected', function () {
cy.get(proposalVoteProgressAgainstPercentage)
.contains('0.00%')
.and('be.visible');
// 1004-VOTE-065
// 1004-VOTE-079
cy.get(proposalVoteProgressForTokens)
.contains(tokensRequiredToAcheiveResult)
.and('be.visible');
@@ -660,6 +834,22 @@ context('Governance flow - with eth and vega wallets connected', function () {
cy.get_proposal_information_from_table('Number of voting parties')
.should('have.text', '1')
.and('be.visible');
cy.get_proposal_information_from_table('Will pass')
.contains('👍')
.should('be.visible');
// 1004-VOTE-062
cy.get_proposal_information_from_table('Majority met')
.contains('👍')
.should('be.visible');
cy.get_proposal_information_from_table('Participation met')
.contains('👍')
.should('be.visible');
// 1004-VOTE-042
// 1004-VOTE-057
// 1004-VOTE-058
// 1004-VOTE-059
// 1004-VOTE-060
cy.contains('Currently set to pass').should('be.visible');
});
});
@@ -852,6 +1042,8 @@ context('Governance flow - with eth and vega wallets connected', function () {
parseSpecialCharSequences: false,
delay: 2,
});
cy.wrap(freeformProposal);
});
});
@@ -1,5 +1,7 @@
const noOpenProposals = '[data-testid="no-open-proposals"]';
const noClosedProposals = '[data-testid="no-closed-proposals"]';
const proposalDocumentationLink = '[data-testid="external-link"]';
const newProposalLink = '[data-testid="new-proposal-link"]';
context('Governance Page - verify elements on page', function () {
before('navigate to governance page', function () {
@@ -15,7 +17,26 @@ context('Governance Page - verify elements on page', function () {
cy.verify_page_header('Governance');
});
it('should have information visible', function () {
it('should be able to see link for - Find out more about Vega governance', function () {
// 1004-VOTE-001
cy.get(proposalDocumentationLink)
.should('be.visible')
.and('have.text', 'Find out more about Vega governance')
.and('have.attr', 'href')
.and('equal', 'https://vega.xyz/governance');
});
it('should be able to see button for - new proposal', function () {
// 1004-VOTE-002
cy.get(newProposalLink)
.should('be.visible')
.and('have.text', 'New proposal')
.and('have.attr', 'href')
.and('equal', '/governance/propose');
});
it('should be able to see that no proposals exist', function () {
// 1004-VOTE-003
cy.get(noOpenProposals)
.should('be.visible')
.and('have.text', 'There are no open or yet to enact proposals');
@@ -170,7 +170,7 @@ context('Vega Wallet - verify elements on widget', function () {
});
});
it('should have wallet name visible', function () {
it.skip('should have wallet name visible', function () {
cy.get(walletContainer).within(() => {
cy.get(walletName)
.should('be.visible')
@@ -238,7 +238,7 @@ context('Vega Wallet - verify elements on widget', function () {
});
});
it('should have vega wallet public key and name visible', function () {
it.skip('should have vega wallet public key and name visible', function () {
cy.get(dialog).within(() => {
cy.get(dialogVegaKey)
.should('be.visible')
@@ -1,19 +1,26 @@
const vegaWalletContainer = '[data-testid="vega-wallet"]';
const restConnectorForm = '[data-testid="rest-connector-form"]';
const vegaWalletNameElement = '[data-testid="wallet-name"]';
const vegaWalletName = Cypress.env('vegaWalletName');
const vegaWalletLocation = Cypress.env('vegaWalletLocation');
const vegaWalletPassphrase = Cypress.env('vegaWalletPassphrase');
Cypress.Commands.add('vega_wallet_import', () => {
cy.highlight(`Importing Vega Wallet ${vegaWalletName}`);
cy.exec(`vegawallet init -f --home ${vegaWalletLocation}`);
cy.exec(`vega wallet init -f --home ${vegaWalletLocation}`);
cy.exec(
`vegawallet import -w ${vegaWalletName} --recovery-phrase-file ./src/fixtures/wallet/recovery -p ./src/fixtures/wallet/passphrase --home ~/.vegacapsule/testnet/wallet`,
`vega wallet import -w ${vegaWalletName} --recovery-phrase-file ./src/fixtures/wallet/recovery -p ./src/fixtures/wallet/passphrase --home ~/.vegacapsule/testnet/wallet`,
{ failOnNonZeroExit: false }
);
cy.exec(
`vegawallet service run --network DV --automatic-consent --home ${vegaWalletLocation}`
`vega wallet service run --network DV --automatic-consent --home ${vegaWalletLocation}`
);
cy.exec(`vega wallet version`)
.its('stdout')
.then((output) => {
cy.log(output);
});
});
Cypress.Commands.add('vega_wallet_connect', () => {
@@ -31,5 +38,5 @@ Cypress.Commands.add('vega_wallet_connect', () => {
cy.get('#passphrase').click().type(vegaWalletPassphrase);
cy.get('button').contains('Connect').click();
});
cy.contains(`${vegaWalletName} key`, { timeout: 20000 }).should('be.visible');
cy.get(vegaWalletNameElement).should('be.visible');
});
+1
View File
@@ -8,3 +8,4 @@ NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/docs/testnet
+1
View File
@@ -8,3 +8,4 @@ NX_ETHERSCAN_URL=https://etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_REST=https://api.token.vega.xyz/
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/docs/mainnet
+1
View File
@@ -8,6 +8,7 @@ NX_VEGA_REST=https://api.n01.stagnet3.vega.xyz
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
NX_VEGA_EXPLORER_URL=https://staging3.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/docs/mainnet
# App flags
NX_EXPLORER_ASSETS=1
+1
View File
@@ -8,3 +8,4 @@ NX_ETHERSCAN_URL=https://ropsten.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_REST=https://api.n11.testnet.vega.xyz
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_DOCS_URL=https://docs.vega.xyz/docs/testnet
+2
View File
@@ -52,7 +52,9 @@ export const ENV = {
branch: windowOrDefault('NX_BRANCH'),
vegaUrl: windowOrDefault('NX_VEGA_URL'),
restUrl: windowOrDefault('NX_VEGA_REST'),
explorerUrl: windowOrDefault('NX_VEGA_EXPLORER'),
urlConnect: TRUTHY.includes(windowOrDefault('NX_ETH_URL_CONNECT')),
docsUrl: windowOrDefault('NX_VEGA_DOCS_URL'),
ethWalletMnemonic: windowOrDefault('NX_ETH_WALLET_MNEMONIC'),
localProviderUrl: windowOrDefault('NX_LOCAL_PROVIDER_URL'),
flags: {
+1 -1
View File
@@ -1,3 +1,3 @@
export * from './flags';
export * from './links';
export * from './network-params';
export * from './env';
+1
View File
@@ -11,4 +11,5 @@ export const Links = {
STAKING_GUIDE:
'https://docs.vega.xyz/docs/mainnet/concepts/vega-chain/#staking-on-vega',
GOVERNANCE_PAGE: 'https://vega.xyz/governance',
PROPOSALS_GUIDE: 'https://docs.vega.xyz/docs/mainnet/tutorials/proposals',
};
-25
View File
@@ -1,25 +0,0 @@
export const NetworkParams = {
ETHEREUM_CONFIG: 'blockchains.ethereumConfig',
REWARD_ASSET: 'reward.asset',
REWARD_PAYOUT_DURATION: 'reward.staking.delegation.payoutDelay',
GOV_UPDATE_MARKET_REQUIRED_MAJORITY:
'governance.proposal.updateMarket.requiredMajority',
GOV_UPDATE_MARKET_REQUIRED_PARTICIPATION:
'governance.proposal.updateMarket.requiredParticipation',
GOV_NEW_MARKET_REQUIRED_MAJORITY:
'governance.proposal.market.requiredMajority',
GOV_NEW_MARKET_REQUIRED_PARTICIPATION:
'governance.proposal.market.requiredParticipation',
GOV_ASSET_REQUIRED_MAJORITY: 'governance.proposal.asset.requiredMajority',
GOV_ASSET_REQUIRED_PARTICIPATION:
'governance.proposal.asset.requiredParticipation',
GOV_UPDATE_NET_PARAM_REQUIRED_MAJORITY:
'governance.proposal.updateNetParam.requiredMajority',
GOV_UPDATE_NET_PARAM_REQUIRED_PARTICIPATION:
'governance.proposal.updateNetParam.requiredParticipation',
GOV_FREEFORM_REQUIRED_PARTICIPATION:
'governance.proposal.freeform.requiredParticipation',
GOV_FREEFORM_REQUIRED_MAJORITY:
'governance.proposal.freeform.requiredMajority',
VALIDATOR_DELEGATION_MIN_AMOUNT: 'validators.delegation.minAmount',
};
+54 -2
View File
@@ -630,8 +630,60 @@
"FilterProposalsDescription": "Filter by proposal ID or proposer ID",
"Freeform proposal": "Freeform proposal",
"NewProposal": "New proposal",
"MinProposalRequirements": "You must have at least 1 VEGA associated to make a proposal",
"ProposalTypeQuestion": "What type of proposal would you like to make?",
"NetworkParameterProposal": "Update network parameter proposal",
"NewMarketProposal": "New market proposal",
"UpdateMarketProposal": "Update market proposal",
"NewAssetProposal": "New asset proposal",
"NewFreeformProposal": "New freeform proposal",
"NewRawProposal": "New raw proposal",
"MinProposalRequirements1": "You must have at least",
"MinProposalRequirements2": "VEGA associated to make a proposal",
"totalSupply": "Total Supply",
"viaWallet": "via wallet",
"viaContract": "via vesting"
"viaContract": "via vesting",
"ProposalDocsPrefix": "For guidance on how to make proposals, see",
"NetworkParameter": "Network parameter",
"NewMarket": "New market",
"UpdateMarket": "Update market",
"NewAsset": "New asset",
"Freeform": "Freeform",
"RawProposal": "Let me choose (raw proposal)",
"UseMin": "Use minimum",
"UseMax": "Use maximum",
"Proposal": "Proposal",
"ProposalRationale": "Proposal rationale",
"ProposalTitle": "Title",
"ProposalTitleText": "Tell people what you are proposing and why (100 characters or less)",
"ProposalsGuide": "proposals guide",
"ProposalDescription": "Description",
"ProposalDescriptionText": "Full justification for what you are proposing (20,000 characters or less). Markdown is recommended. When linking to external resources please use IPFS",
"ProposalTerms": "Proposal terms (JSON format)",
"ProposalTermsText": "For more information visit the",
"ProposalReference": "Reference",
"ProposalVoteTitle": "Vote deadline",
"ProposalVoteAndEnactmentTitle": "Vote deadline and enactment",
"ProposalVoteDeadline": "Time till voting closes",
"ProposalEnactmentDeadline": "Time till enactment (after vote close)",
"ProposalValidationDeadline": "Time till ERC-20 asset validation. Maximum value is affected by the vote deadline.",
"ThisWillSetVotingDeadlineTo": "This will set the voting deadline to",
"ThisWillSetEnactmentDeadlineTo": "This will set the enactment date to",
"ThisWillSetValidationDeadlineTo": "This will set the validation deadline to",
"Hours": "hours",
"ThisWillAdd2MinutesToAllowTimeToConfirmInWallet": "Note: we add 2 minutes of extra time when you choose the minimum value. This gives you time to confirm the proposal in your wallet.",
"SelectAMarketToChange": "Select a market to change",
"MarketName": "Market name",
"MarketCode": "Market code",
"MarketId": "Market ID",
"ProposeNewMarketTerms": "terms.changes.newMarket (JSON format)",
"ProposeUpdateMarketTerms": "terms.updateMarket.changes (JSON format)",
"SelectAParameterToChange": "Select a parameter to change",
"SelectParameter": "Select parameter",
"SelectMarket": "Select market",
"CurrentValue": "Current value",
"NewProposedValue": "New proposed value",
"MoreProposalsInfo": "To see Explorer data on proposals visit",
"MoreNetParamsInfo": "To see Explorer data on network params visit",
"MoreMarketsInfo": "To see Explorer data on existing markets visit",
"MoreAssetsInfo": "To see Explorer data on existing assets visit"
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { ENV } from '../config/env';
import { ENV } from '../config';
export function getDataNodeUrl() {
const base = ENV.vegaUrl;
@@ -66,7 +66,11 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
{t(
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network.`
)}{' '}
<ExternalLink href={Links.GOVERNANCE_PAGE} className="text-white">
<ExternalLink
data-testid="proposal-documentation-link"
href={Links.GOVERNANCE_PAGE}
className="text-white"
>
{t(`Find out more about Vega governance`)}
</ExternalLink>
</p>
@@ -0,0 +1,9 @@
export * from './proposal-docs-link';
export * from './proposal-form-subheader';
export * from './proposal-form-min-requirements';
export * from './proposal-form-title';
export * from './proposal-form-description';
export * from './proposal-form-terms';
export * from './proposal-form-submit';
export * from './proposal-form-transaction-dialog';
export * from './proposal-form-vote-and-enactment-deadline';
@@ -0,0 +1,16 @@
import { render, screen } from '@testing-library/react';
import { ProposalDocsLink } from './proposal-docs-link';
describe('Proposal Docs Link', () => {
it('should render successfully', () => {
render(<ProposalDocsLink urlPart1="https://test.com" />);
expect(screen.getByTestId('link')).toBeTruthy();
});
it('should display link text', () => {
render(<ProposalDocsLink urlPart1="https://test.com" />);
expect(
screen.getByText('https://test.com/tutorials/proposals')
).toBeInTheDocument();
});
});
@@ -0,0 +1,26 @@
import classnames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Link } from '@vegaprotocol/ui-toolkit';
interface ProposalDocsLinkProps {
urlPart1: string;
urlPart2?: string;
}
export const ProposalDocsLink = ({
urlPart1,
urlPart2,
}: ProposalDocsLinkProps) => {
const { t } = useTranslation();
const docsLink = `${urlPart1}${urlPart2 ? urlPart2 : '/tutorials/proposals'}`;
const linkStyles = classnames('underline mb-2');
return (
<p>
{t('ProposalDocsPrefix')}{' '}
<Link href={docsLink} target="_blank" className={linkStyles}>
{docsLink}
</Link>
</p>
);
};
@@ -0,0 +1,26 @@
import { render, screen } from '@testing-library/react';
import { ProposalFormDescription } from './proposal-form-description';
describe('Proposal Form Description', () => {
it('should render successfully', () => {
const register = jest.fn();
render(
<ProposalFormDescription
registerField={register('proposalDescription')}
errorMessage={undefined}
/>
);
expect(screen.getByTestId('proposal-description')).toBeTruthy();
});
it('should display error text', () => {
const register = jest.fn();
render(
<ProposalFormDescription
registerField={register('proposalDescription')}
errorMessage="Error text"
/>
);
expect(screen.getByText('Error text')).toBeInTheDocument();
});
});
@@ -0,0 +1,32 @@
import { useTranslation } from 'react-i18next';
import { FormGroup, InputError, TextArea } from '@vegaprotocol/ui-toolkit';
import type { UseFormRegisterReturn } from 'react-hook-form';
interface ProposalFormDescriptionProps {
registerField: UseFormRegisterReturn<'proposalDescription'>;
errorMessage: string | undefined;
}
export const ProposalFormDescription = function ({
registerField: register,
errorMessage,
}: ProposalFormDescriptionProps) {
const { t } = useTranslation();
return (
<FormGroup
label={t('ProposalDescription')}
labelDescription={t('ProposalDescriptionText')}
labelFor="proposal-description"
>
<TextArea
id="proposal-description"
maxLength={20000}
className="min-h-[200px]"
hasError={Boolean(errorMessage)}
data-testid="proposal-description"
{...register}
/>
{errorMessage && <InputError intent="danger">{errorMessage}</InputError>}
</FormGroup>
);
};
@@ -0,0 +1,32 @@
import { render } from '@testing-library/react';
import { ProposalFormMinRequirements } from './proposal-form-min-requirements';
describe('ProposalFormMinRequirements', () => {
it('should render successfully with spam protection value, if larger', () => {
const { baseElement } = render(
<ProposalFormMinRequirements
minProposerBalance="1000000000000000000"
spamProtectionMin="2000000000000000000"
/>
);
expect(baseElement).toBeTruthy();
expect(baseElement).toHaveTextContent(
'You must have at least 2 VEGA associated to make a proposal'
);
});
it('should render successfully with min proposer value, if larger', () => {
const { baseElement } = render(
<ProposalFormMinRequirements
minProposerBalance="3000000000000000000"
spamProtectionMin="1000000000000000000"
/>
);
expect(baseElement).toBeTruthy();
expect(baseElement).toHaveTextContent(
'You must have at least 3 VEGA associated to make a proposal'
);
});
});
@@ -0,0 +1,29 @@
import { useTranslation } from 'react-i18next';
import { addDecimal } from '@vegaprotocol/react-helpers';
interface ProposalFormMinRequirementsProps {
minProposerBalance: string | undefined;
spamProtectionMin: string | undefined;
}
export const ProposalFormMinRequirements = ({
minProposerBalance,
spamProtectionMin,
}: ProposalFormMinRequirementsProps) => {
const { t } = useTranslation();
const minProposerBalanceFormatted =
minProposerBalance && Number(addDecimal(minProposerBalance, 18));
const spamProtectionMinFormatted =
spamProtectionMin && Number(addDecimal(spamProtectionMin, 18));
const larger =
Number(minProposerBalanceFormatted) > (spamProtectionMinFormatted || 0)
? minProposerBalanceFormatted
: spamProtectionMinFormatted;
return (
<p className="mb-4">{`${t('MinProposalRequirements1')} ${larger} ${t(
'MinProposalRequirements2'
)}`}</p>
);
};
@@ -0,0 +1,9 @@
import { render, screen } from '@testing-library/react';
import { ProposalFormSubheader } from './proposal-form-subheader';
describe('Proposal Form Subheader', () => {
it('should display subheader text', () => {
render(<ProposalFormSubheader>Test</ProposalFormSubheader>);
expect(screen.getByText('Test')).toBeInTheDocument();
});
});
@@ -0,0 +1,9 @@
import type { ReactNode } from 'react';
interface ProposalFormSubheaderProps {
children: ReactNode;
}
export const ProposalFormSubheader = ({
children,
}: ProposalFormSubheaderProps) => <h2 className="mt-8 mb-4">{children}</h2>;
@@ -0,0 +1,19 @@
import { render, screen } from '@testing-library/react';
import { ProposalFormSubmit } from './proposal-form-submit';
describe('Proposal Form Submit', () => {
it('should render successfully', () => {
render(<ProposalFormSubmit isSubmitting={false} />);
expect(screen.getByTestId('proposal-submit')).toBeTruthy();
});
it('should display submitting text', () => {
render(<ProposalFormSubmit isSubmitting={true} />);
expect(screen.getByText('Submitting Proposal')).toBeInTheDocument();
});
it('should display submit text', () => {
render(<ProposalFormSubmit isSubmitting={false} />);
expect(screen.getByText('Submit Proposal')).toBeInTheDocument();
});
});
@@ -0,0 +1,24 @@
import { useTranslation } from 'react-i18next';
import { Button } from '@vegaprotocol/ui-toolkit';
interface ProposalFormSubmitProps {
isSubmitting: boolean;
}
export const ProposalFormSubmit = ({
isSubmitting,
}: ProposalFormSubmitProps) => {
const { t } = useTranslation();
return (
<div className="mt-10 my-20">
<Button
variant="primary"
type="submit"
data-testid="proposal-submit"
disabled={isSubmitting}
>
{isSubmitting ? t('Submitting') : t('Submit')} {t('Proposal')}
</Button>
</div>
);
};
@@ -0,0 +1,26 @@
import { render, screen } from '@testing-library/react';
import { ProposalFormTerms } from './proposal-form-terms';
describe('Proposal Form Terms', () => {
it('should render successfully', () => {
const register = jest.fn();
render(
<ProposalFormTerms
registerField={register('proposalTerms')}
errorMessage={undefined}
/>
);
expect(screen.getByTestId('proposal-terms')).toBeTruthy();
});
it('should display error text', () => {
const register = jest.fn();
render(
<ProposalFormTerms
registerField={register('proposalTerms')}
errorMessage="Error text"
/>
);
expect(screen.getByText('Error text')).toBeInTheDocument();
});
});
@@ -0,0 +1,38 @@
import { useTranslation } from 'react-i18next';
import { FormGroup, InputError, TextArea } from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment';
import { ProposalDocsLink } from './proposal-docs-link';
import type { UseFormRegisterReturn } from 'react-hook-form';
interface ProposalFormTermsProps {
registerField: UseFormRegisterReturn<'proposalTerms'>;
errorMessage: string | undefined;
labelOverride?: string;
}
export const ProposalFormTerms = function ({
registerField: register,
errorMessage,
labelOverride,
}: ProposalFormTermsProps) {
const { VEGA_DOCS_URL } = useEnvironment();
const { t } = useTranslation();
return (
<FormGroup
label={labelOverride || t('ProposalTerms')}
labelFor="proposal-terms"
>
<div className="mt-[-4px] mb-2 text-sm font-light">
{VEGA_DOCS_URL && <ProposalDocsLink urlPart1={VEGA_DOCS_URL} />}
</div>
<TextArea
id="proposal-terms"
className="min-h-[200px]"
hasError={Boolean(errorMessage)}
data-testid="proposal-terms"
{...register}
/>
{errorMessage && <InputError intent="danger">{errorMessage}</InputError>}
</FormGroup>
);
};
@@ -0,0 +1,26 @@
import { render, screen } from '@testing-library/react';
import { ProposalFormTitle } from './proposal-form-title';
describe('Proposal Form Title', () => {
it('should render successfully', () => {
const register = jest.fn();
render(
<ProposalFormTitle
registerField={register('proposalTitle')}
errorMessage={undefined}
/>
);
expect(screen.getByTestId('proposal-title')).toBeTruthy();
});
it('should display error text', () => {
const register = jest.fn();
render(
<ProposalFormTitle
registerField={register('proposalTitle')}
errorMessage="Error text"
/>
);
expect(screen.getByText('Error text')).toBeInTheDocument();
});
});
@@ -0,0 +1,31 @@
import { useTranslation } from 'react-i18next';
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
import type { UseFormRegisterReturn } from 'react-hook-form';
interface ProposalFormTitleProps {
registerField: UseFormRegisterReturn<'proposalTitle'>;
errorMessage: string | undefined;
}
export const ProposalFormTitle = function ({
registerField: register,
errorMessage,
}: ProposalFormTitleProps) {
const { t } = useTranslation();
return (
<FormGroup
label={t('ProposalTitle')}
labelFor="proposal-title"
labelDescription={t('ProposalTitleText')}
>
<Input
id="proposal-title"
maxLength={100}
hasError={Boolean(errorMessage)}
data-testid="proposal-title"
{...register}
/>
{errorMessage && <InputError intent="danger">{errorMessage}</InputError>}
</FormGroup>
);
};
@@ -0,0 +1,18 @@
import { render, screen } from '@testing-library/react';
import { ProposalFormTransactionDialog } from './proposal-form-transaction-dialog';
const mockTransactionDialog = jest
.fn()
.mockImplementation(() => <div>Mock Transaction Dialog</div>);
describe('Proposal Form Transaction Dialog', () => {
it('should render', () => {
render(
<ProposalFormTransactionDialog
finalizedProposal={null}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.getByText('Mock Transaction Dialog')).toBeTruthy();
});
});
@@ -0,0 +1,37 @@
import {
getProposalDialogIcon,
getProposalDialogIntent,
getProposalDialogTitle,
} from '@vegaprotocol/governance';
import { t } from '@vegaprotocol/react-helpers';
import { ProposalState } from '@vegaprotocol/types';
import type { ProposalEvent_busEvents_event_Proposal } from '@vegaprotocol/governance';
import type { DialogProps } from '@vegaprotocol/wallet';
interface ProposalFormTransactionDialogProps {
finalizedProposal: ProposalEvent_busEvents_event_Proposal | null;
TransactionDialog: (props: DialogProps) => JSX.Element;
}
export const ProposalFormTransactionDialog = ({
finalizedProposal,
TransactionDialog,
}: ProposalFormTransactionDialogProps) => (
<div data-testid="proposal-transaction-dialog">
{finalizedProposal?.rejectionReason ? (
<TransactionDialog
title={t('Proposal rejected')}
intent={getProposalDialogIntent(ProposalState.STATE_REJECTED)}
icon={getProposalDialogIcon(ProposalState.STATE_REJECTED)}
>
<p>{finalizedProposal.rejectionReason}</p>
</TransactionDialog>
) : (
<TransactionDialog
title={getProposalDialogTitle(finalizedProposal?.state)}
intent={getProposalDialogIntent(finalizedProposal?.state)}
icon={getProposalDialogIcon(finalizedProposal?.state)}
/>
)}
</div>
);
@@ -0,0 +1,194 @@
import { render, screen, fireEvent, act } from '@testing-library/react';
import { ProposalFormVoteAndEnactmentDeadline } from './proposal-form-vote-and-enactment-deadline';
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterEach(() => {
jest.useRealTimers();
});
const minVoteDeadline = '1h0m0s';
const maxVoteDeadline = '5h0m0s';
const minEnactDeadline = '1h0m0s';
const maxEnactDeadline = '4h0m0s';
const renderComponent = () => {
const register = jest.fn();
render(
<ProposalFormVoteAndEnactmentDeadline
voteRegister={register('proposalVoteDeadline')}
voteErrorMessage={undefined}
voteMinClose={minVoteDeadline}
voteMaxClose={maxVoteDeadline}
enactmentRegister={register('proposalEnactmentDeadline')}
enactmentErrorMessage={undefined}
enactmentMinClose={minEnactDeadline}
enactmentMaxClose={maxEnactDeadline}
validationRequired={true}
validationRegister={register('proposalValidationDeadline')}
validationErrorMessage={undefined}
/>
);
};
describe('Proposal form vote, validation and enactment deadline', () => {
it('should render all component parts successfully', () => {
renderComponent();
expect(screen.getByTestId('proposal-vote-deadline')).toBeTruthy();
expect(screen.getByTestId('proposal-enactment-deadline')).toBeTruthy();
expect(screen.getByTestId('proposal-validation-deadline')).toBeTruthy();
});
it('should have the correct initial values', () => {
renderComponent();
const deadlineInput = screen.getByTestId('proposal-vote-deadline');
expect(deadlineInput).toHaveValue(1);
const enactmentDeadlineInput = screen.getByTestId(
'proposal-enactment-deadline'
);
expect(enactmentDeadlineInput).toHaveValue(1);
const validationDeadlineInput = screen.getByTestId(
'proposal-validation-deadline'
);
expect(validationDeadlineInput).toHaveValue(0);
});
it('should use the voting max and min values when the buttons are clicked', () => {
renderComponent();
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
const maxButton = screen.getByTestId('max-vote');
const minButton = screen.getByTestId('min-vote');
fireEvent.click(maxButton);
expect(voteDeadlineInput).toHaveValue(5);
fireEvent.click(minButton);
expect(voteDeadlineInput).toHaveValue(1);
});
it('should use the validation max and min values when the buttons are clicked', () => {
renderComponent();
const validationDeadlineInput = screen.getByTestId(
'proposal-validation-deadline'
);
const maxButton = screen.getByTestId('max-validation');
const minButton = screen.getByTestId('min-validation');
fireEvent.click(maxButton);
// Note: the validation max value is determined by the vote deadline max value,
// which will currently be the default value of 1
expect(validationDeadlineInput).toHaveValue(1);
fireEvent.click(minButton);
expect(validationDeadlineInput).toHaveValue(0);
});
it('should update the validation deadline max value when the vote deadline value changes', () => {
renderComponent();
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
const validationDeadlineInput = screen.getByTestId(
'proposal-validation-deadline'
);
const maxButton = screen.getByTestId('max-validation');
fireEvent.change(voteDeadlineInput, { target: { value: 2 } });
fireEvent.click(maxButton);
expect(validationDeadlineInput).toHaveValue(2);
});
it('should use the enactment max and min values when the buttons are clicked', () => {
renderComponent();
const enactmentDeadlineInput = screen.getByTestId(
'proposal-enactment-deadline'
);
const maxButton = screen.getByTestId('max-enactment');
const minButton = screen.getByTestId('min-enactment');
fireEvent.click(maxButton);
expect(enactmentDeadlineInput).toHaveValue(4);
fireEvent.click(minButton);
expect(enactmentDeadlineInput).toHaveValue(1);
});
it('should show the 2 mins extra message when the vote deadline is set to minimum', () => {
renderComponent();
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
const minButton = screen.getByTestId('min-vote');
fireEvent.click(minButton);
expect(voteDeadlineInput).toHaveValue(1);
expect(screen.getByTestId('voting-2-mins-extra')).toBeTruthy();
});
it('should show the 2 mins extra message when the validation deadline is set to minimum', () => {
renderComponent();
const validationDeadlineInput = screen.getByTestId(
'proposal-validation-deadline'
);
const minButton = screen.getByTestId('min-validation');
fireEvent.click(minButton);
expect(validationDeadlineInput).toHaveValue(0);
expect(screen.getByTestId('validation-2-mins-extra')).toBeTruthy();
});
it('should show the correct datetimes', () => {
renderComponent();
// Should be adding 2 mins to the vote deadline as the minimum is set by
// default, and we add 2 mins for wallet confirmation
expect(screen.getByTestId('voting-date')).toHaveTextContent(
'1/1/1970, 2:02:00 AM'
);
expect(screen.getByTestId('validation-date')).toHaveTextContent(
'1/1/1970, 1:02:00 AM'
);
expect(screen.getByTestId('enactment-date')).toHaveTextContent(
'1/1/1970, 3:00:00 AM'
);
});
it('should be updating every second, so show the correct datetimes when 30 seconds have passed', () => {
renderComponent();
act(() => {
jest.advanceTimersByTime(30000);
});
expect(screen.getByTestId('voting-date')).toHaveTextContent(
'1/1/1970, 2:02:30 AM'
);
expect(screen.getByTestId('validation-date')).toHaveTextContent(
'1/1/1970, 1:02:30 AM'
);
expect(screen.getByTestId('enactment-date')).toHaveTextContent(
'1/1/1970, 3:00:30 AM'
);
});
it('update the vote deadline date and the enactment deadline date when the vote deadline is changed', () => {
renderComponent();
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
fireEvent.change(voteDeadlineInput, { target: { value: 2 } });
expect(screen.getByTestId('voting-date')).toHaveTextContent(
'1/1/1970, 3:00:00 AM'
);
expect(screen.getByTestId('enactment-date')).toHaveTextContent(
'1/1/1970, 4:00:00 AM'
);
});
it('updates the validation deadline max and date when the vote deadline max is changed', () => {
renderComponent();
const voteDeadlineMinButton = screen.getByTestId('min-vote');
const voteDeadlineMaxButton = screen.getByTestId('max-vote');
const validationDeadlineMaxButton = screen.getByTestId('max-validation');
const validationDeadlineInput = screen.getByTestId(
'proposal-validation-deadline'
);
fireEvent.click(voteDeadlineMaxButton);
fireEvent.click(validationDeadlineMaxButton);
expect(screen.getByTestId('validation-date')).toHaveTextContent(
'1/1/1970, 6:00:00 AM'
);
expect(validationDeadlineInput).toHaveValue(5);
fireEvent.click(voteDeadlineMinButton);
expect(screen.getByTestId('validation-date')).toHaveTextContent(
'1/1/1970, 2:00:00 AM'
);
expect(validationDeadlineInput).toHaveValue(1);
});
});
@@ -0,0 +1,380 @@
import { useEffect, useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { parse as ISO8601Parse, toSeconds } from 'iso8601-duration';
import {
ButtonLink,
FormGroup,
Input,
InputError,
} from '@vegaprotocol/ui-toolkit';
import { addHours, addMinutes } from 'date-fns';
import { ProposalFormSubheader } from './proposal-form-subheader';
import type { UseFormRegisterReturn } from 'react-hook-form';
export interface ProposalFormVoteAndEnactmentDeadlineProps {
voteRegister: UseFormRegisterReturn<'proposalVoteDeadline'>;
voteErrorMessage: string | undefined;
voteMinClose: string;
voteMaxClose: string;
enactmentRequired?: boolean;
enactmentRegister?: UseFormRegisterReturn<'proposalEnactmentDeadline'>;
enactmentErrorMessage?: string;
enactmentMinClose?: string;
enactmentMaxClose?: string;
validationRequired?: boolean;
validationRegister?: UseFormRegisterReturn<'proposalValidationDeadline'>;
validationErrorMessage?: string;
}
export const ProposalFormVoteAndEnactmentDeadline = function ({
voteRegister,
voteErrorMessage,
voteMinClose,
voteMaxClose,
enactmentRegister,
enactmentErrorMessage,
enactmentMinClose,
enactmentMaxClose,
validationRequired,
validationRegister,
validationErrorMessage,
}: ProposalFormVoteAndEnactmentDeadlineProps) {
const {
minVoteSeconds,
maxVoteSeconds,
minEnactmentSeconds,
maxEnactmentSeconds,
} = useMemo(
() => ({
minVoteSeconds: toSeconds(
ISO8601Parse(`PT${voteMinClose.toUpperCase()}`)
),
maxVoteSeconds: toSeconds(
ISO8601Parse(`PT${voteMaxClose.toUpperCase()}`)
),
minEnactmentSeconds:
enactmentMinClose &&
toSeconds(ISO8601Parse(`PT${enactmentMinClose.toUpperCase()}`)),
maxEnactmentSeconds:
enactmentMaxClose &&
toSeconds(ISO8601Parse(`PT${enactmentMaxClose.toUpperCase()}`)),
}),
[voteMinClose, voteMaxClose, enactmentMinClose, enactmentMaxClose]
);
// As we're rounding to hours for the simplified deadline ui presently, if vote deadline
// is less than one hour, make it one hour.
const { minVoteHours, maxVoteHours, minEnactmentHours, maxEnactmentHours } =
useMemo(
() => ({
minVoteHours:
Math.floor(minVoteSeconds / 3600) > 1
? Math.floor(minVoteSeconds / 3600)
: 1,
maxVoteHours: Math.floor(maxVoteSeconds / 3600),
minEnactmentHours:
minEnactmentSeconds && Math.floor(minEnactmentSeconds / 3600) > 1
? Math.floor(minEnactmentSeconds / 3600)
: 1,
maxEnactmentHours:
maxEnactmentSeconds && Math.floor(maxEnactmentSeconds / 3600),
}),
[minVoteSeconds, maxVoteSeconds, minEnactmentSeconds, maxEnactmentSeconds]
);
const [deadlines, setDeadlines] = useState<{
vote: number;
enactment: number;
validation: number;
}>({
vote: minVoteHours,
enactment: minEnactmentHours,
validation: 0,
});
const [deadlineDates, setDeadlineDates] = useState<{
vote: Date;
enactment: Date;
validation: Date;
}>({
vote:
deadlines.vote === minVoteHours
? addHours(addMinutes(new Date(), 2), deadlines.vote)
: addHours(new Date(), deadlines.vote),
enactment: addHours(new Date(), deadlines.vote + deadlines.enactment),
validation:
deadlines.validation === 0
? addHours(addMinutes(new Date(), 2), deadlines.validation)
: addHours(new Date(), deadlines.validation),
});
useEffect(() => {
const interval = setInterval(() => {
setDeadlineDates((prev) => ({
...prev,
vote:
deadlines.vote === minVoteHours
? addHours(addMinutes(new Date(), 2), deadlines.vote)
: addHours(new Date(), deadlines.vote),
enactment: addHours(new Date(), deadlines.vote + deadlines.enactment),
validation:
deadlines.validation === 0
? addHours(addMinutes(new Date(), 2), deadlines.validation)
: addHours(new Date(), deadlines.validation),
}));
}, 1000);
return () => clearInterval(interval);
}, [deadlines, minVoteHours]);
const updateVoteDeadlineAndDate = (hours: number) => {
// Validation, when needed, can only happen within the voting period. Therefore, if the
// vote deadline is changed, the validation deadline must be changed to be within the
// new vote deadline.
setDeadlines((prev) => ({
...prev,
vote: hours,
validation: Math.min(prev.validation, hours),
}));
// If the vote deadline is set to minimum, add 2 mins to the date as we do
// this on submission to allow time to confirm in the wallet. Amending the
// vote deadline also changes the enactment date and potentially the validation
// date.
// The validation deadline date cannot be after the vote deadline date. Therefore,
// if the vote deadline is changed, the validation deadline must potentially
// be changed to be within the new vote deadline.
setDeadlineDates((prev) => ({
...prev,
vote:
hours === minVoteHours
? addHours(addMinutes(new Date(), 2), hours)
: addHours(new Date(), hours),
enactment: addHours(new Date(), hours + deadlines.enactment),
validation: addHours(new Date(), Math.min(hours, deadlines.validation)),
}));
};
const updateEnactmentDeadlineAndDate = (hours: number) => {
setDeadlines((prev) => ({
...prev,
enactment: hours,
}));
setDeadlineDates((prev) => ({
...prev,
enactment: addHours(deadlineDates.vote, hours),
}));
};
const updateValidationDeadlineAndDate = (hours: number) => {
setDeadlines((prev) => ({
...prev,
validation: hours,
}));
setDeadlineDates((prev) => ({
...prev,
validation:
hours === 0
? addHours(addMinutes(new Date(), 2), hours)
: addHours(new Date(), hours),
}));
};
const { t } = useTranslation();
return (
<>
<ProposalFormSubheader>
{enactmentRegister && enactmentMinClose && enactmentMaxClose
? t('ProposalVoteAndEnactmentTitle')
: t('ProposalVoteTitle')}
</ProposalFormSubheader>
<FormGroup
label={t('ProposalVoteDeadline')}
labelFor="proposal-vote-deadline"
>
<div className="flex items-center gap-2">
<div className="relative w-28">
<Input
{...voteRegister}
id="proposal-vote-deadline"
type="number"
value={deadlines.vote}
min={minVoteHours}
max={maxVoteHours}
onChange={(e) => {
updateVoteDeadlineAndDate(Number(e.target.value));
}}
data-testid="proposal-vote-deadline"
className="pr-12"
/>
<span className="absolute right-2 bottom-1/2 translate-y-1/2 text-sm">
{t('Hours')}
</span>
</div>
<div className="flex items-center gap-4 text-sm">
<ButtonLink
data-testid="min-vote"
onClick={() => updateVoteDeadlineAndDate(minVoteHours)}
>
{t('UseMin')}
</ButtonLink>
<ButtonLink
data-testid="max-vote"
onClick={() => updateVoteDeadlineAndDate(maxVoteHours)}
>
{t('UseMax')}
</ButtonLink>
</div>
</div>
{voteErrorMessage && (
<InputError intent="danger">{voteErrorMessage}</InputError>
)}
{deadlineDates.vote && (
<p className="mt-2 text-sm text-white">
<span className="font-light">
{t('ThisWillSetVotingDeadlineTo')}
</span>
<span data-testid="voting-date" className="pl-2">
{deadlineDates.vote?.toLocaleString()}
</span>
{deadlines.vote === minVoteHours && (
<span
data-testid="voting-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
</p>
)}
</FormGroup>
{validationRequired && (
<FormGroup
label={t('ProposalValidationDeadline')}
labelFor="proposal-validation-deadline"
>
<div className="flex items-center gap-2">
<div className="relative w-28">
<Input
{...validationRegister}
id="proposal-validation-deadline"
type="number"
value={deadlines.validation}
min={0}
max={deadlines.vote}
onChange={(e) => {
updateValidationDeadlineAndDate(Number(e.target.value));
}}
data-testid="proposal-validation-deadline"
className="pr-12"
/>
<span className="absolute right-2 bottom-1/2 translate-y-1/2 text-sm">
{t('Hours')}
</span>
</div>
<div className="flex items-center gap-4 text-sm">
<ButtonLink
data-testid="min-validation"
onClick={() => updateValidationDeadlineAndDate(0)}
>
{t('UseMin')}
</ButtonLink>
<ButtonLink
data-testid="max-validation"
onClick={() => updateValidationDeadlineAndDate(deadlines.vote)}
>
{t('UseMax')}
</ButtonLink>
</div>
</div>
{validationErrorMessage && (
<InputError intent="danger">{validationErrorMessage}</InputError>
)}
{deadlineDates.validation && (
<p className="mt-2 text-sm text-white">
<span className="font-light">
{t('ThisWillSetValidationDeadlineTo')}
</span>
<span data-testid="validation-date" className="pl-2">
{deadlineDates.validation?.toLocaleString()}
</span>
{deadlines.validation === 0 && (
<span
data-testid="validation-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
</p>
)}
</FormGroup>
)}
{enactmentMinClose && enactmentMaxClose && maxEnactmentHours && (
<FormGroup
label={t('ProposalEnactmentDeadline')}
labelFor="proposal-enactment-deadline"
>
<div className="flex items-center gap-2">
<div className="relative w-28">
<Input
{...enactmentRegister}
id="proposal-enactment-deadline"
type="number"
value={deadlines.enactment}
min={minEnactmentHours}
max={maxEnactmentHours}
onChange={(e) => {
updateEnactmentDeadlineAndDate(Number(e.target.value));
}}
data-testid="proposal-enactment-deadline"
className="pr-12"
/>
<span className="absolute right-2 bottom-1/2 translate-y-1/2 text-sm">
{t('Hours')}
</span>
</div>
<div className="flex items-center gap-4 text-sm">
<ButtonLink
data-testid="min-enactment"
onClick={() =>
updateEnactmentDeadlineAndDate(minEnactmentHours)
}
>
{t('UseMin')}
</ButtonLink>
<ButtonLink
data-testid="max-enactment"
onClick={() =>
updateEnactmentDeadlineAndDate(maxEnactmentHours)
}
>
{t('UseMax')}
</ButtonLink>
</div>
</div>
{enactmentErrorMessage && (
<InputError intent="danger">{enactmentErrorMessage}</InputError>
)}
{deadlineDates.enactment && (
<p className="mt-2 text-sm text-white">
<span className="font-light">
{t('ThisWillSetEnactmentDeadlineTo')}
</span>
<span data-testid="enactment-date" className="pl-2">
{deadlineDates.enactment?.toLocaleString()}
</span>
</p>
)}
</FormGroup>
)}
</>
);
};
@@ -1,7 +1,6 @@
import { useNetworkParams } from '@vegaprotocol/react-helpers';
import { useNetworkParams, NetworkParams } from '@vegaprotocol/react-helpers';
import React from 'react';
import { NetworkParams } from '../../../config';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import { BigNumber } from '../../../lib/bignumber';
import { addDecimal } from '../../../lib/decimals';
@@ -16,63 +15,63 @@ const useProposalNetworkParams = ({
}: {
proposal: ProposalFields;
}) => {
const { data, loading } = useNetworkParams([
NetworkParams.GOV_UPDATE_MARKET_REQUIRED_MAJORITY,
NetworkParams.GOV_UPDATE_MARKET_REQUIRED_PARTICIPATION,
NetworkParams.GOV_NEW_MARKET_REQUIRED_MAJORITY,
NetworkParams.GOV_NEW_MARKET_REQUIRED_PARTICIPATION,
NetworkParams.GOV_ASSET_REQUIRED_MAJORITY,
NetworkParams.GOV_ASSET_REQUIRED_PARTICIPATION,
NetworkParams.GOV_UPDATE_NET_PARAM_REQUIRED_MAJORITY,
NetworkParams.GOV_UPDATE_NET_PARAM_REQUIRED_PARTICIPATION,
NetworkParams.GOV_FREEFORM_REQUIRED_MAJORITY,
NetworkParams.GOV_FREEFORM_REQUIRED_PARTICIPATION,
const { params } = useNetworkParams([
NetworkParams.governance_proposal_updateMarket_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredParticipation,
NetworkParams.governance_proposal_market_requiredMajority,
NetworkParams.governance_proposal_market_requiredParticipation,
NetworkParams.governance_proposal_asset_requiredMajority,
NetworkParams.governance_proposal_asset_requiredParticipation,
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
NetworkParams.governance_proposal_updateNetParam_requiredParticipation,
NetworkParams.governance_proposal_freeform_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredParticipation,
]);
if (loading || !data) {
if (!params) {
return {
requiredMajority: new BigNumber(1),
requiredParticipation: new BigNumber(1),
};
}
const [
updateMarketMajority,
updateMarketParticipation,
newMarketMajority,
newMarketParticipation,
assetMajority,
assetParticipation,
paramMajority,
paramParticipation,
freeformMajority,
freeformParticipation,
] = data;
switch (proposal.terms.change.__typename) {
case 'UpdateMarket':
return {
requiredMajority: updateMarketMajority,
requiredParticipation: new BigNumber(updateMarketParticipation),
requiredMajority:
params.governance_proposal_updateMarket_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_updateMarket_requiredParticipation
),
};
case 'UpdateNetworkParameter':
return {
requiredMajority: paramMajority,
requiredParticipation: new BigNumber(paramParticipation),
requiredMajority:
params.governance_proposal_updateNetParam_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_updateNetParam_requiredParticipation
),
};
case 'NewAsset':
return {
requiredMajority: assetMajority,
requiredParticipation: new BigNumber(assetParticipation),
requiredMajority: params.governance_proposal_asset_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_asset_requiredParticipation
),
};
case 'NewMarket':
return {
requiredMajority: newMarketMajority,
requiredParticipation: new BigNumber(newMarketParticipation),
requiredMajority: params.governance_proposal_market_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_market_requiredParticipation
),
};
case 'NewFreeform':
return {
requiredMajority: freeformMajority,
requiredParticipation: freeformParticipation,
requiredMajority: params.governance_proposal_freeform_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_freeform_requiredParticipation
),
};
default:
throw new Error('Unknown proposal type');
@@ -0,0 +1,4 @@
export {
ProposeFreeform,
ProposeFreeform as default,
} from './propose-freeform';
@@ -0,0 +1,77 @@
import { render, screen } from '@testing-library/react';
import { ProposeFreeform } from './propose-freeform';
import { MockedProvider } from '@apollo/client/testing';
import {
mockWalletContext,
networkParamsQueryMock,
} from '../../test-helpers/mocks';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { BrowserRouter as Router } from 'react-router-dom';
jest.mock('../../../../hooks/use-network-param.tsx', () => ({
useNetworkParamWithKeys: () => ({
data: [
{
__typename: 'NetworkParameter',
key: 'governance.proposal.freeform.maxClose',
value: '8760h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.freeform.maxEnact',
value: '8760h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.freeform.minClose',
value: '1h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.freeform.minEnact',
value: '2h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.freeform.minProposerBalance',
value: '1',
},
{
__typename: 'NetworkParameter',
key: 'spam.protection.freeform.min.tokens',
value: '1000000000000000000',
},
],
loading: false,
error: undefined,
}),
}));
const renderComponent = () =>
render(
<Router>
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposeFreeform />
</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 Freeform', () => {
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy();
});
it('should render the title', () => {
renderComponent();
expect(screen.getByText('New freeform proposal')).toBeTruthy();
});
});
@@ -0,0 +1,133 @@
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
getClosingTimestamp,
useProposalSubmit,
} from '@vegaprotocol/governance';
import { useEnvironment } from '@vegaprotocol/environment';
import {
ProposalFormSubheader,
ProposalFormMinRequirements,
ProposalFormTitle,
ProposalFormDescription,
ProposalFormSubmit,
ProposalFormTransactionDialog,
ProposalFormVoteAndEnactmentDeadline,
ProposalDocsLink,
} from '../../components/propose';
import { AsyncRenderer, Link } from '@vegaprotocol/ui-toolkit';
import { Heading } from '../../../../components/heading';
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
import type { ProposalFreeformTerms } from '@vegaprotocol/wallet';
import { useNetworkParams, NetworkParams } from '@vegaprotocol/react-helpers';
export interface FreeformProposalFormFields {
proposalVoteDeadline: string;
proposalTitle: string;
proposalDescription: string;
proposalTerms: string;
proposalReference: string;
}
export const ProposeFreeform = () => {
const { params, loading, error } = useNetworkParams([
NetworkParams.governance_proposal_freeform_minClose,
NetworkParams.governance_proposal_freeform_maxClose,
NetworkParams.governance_proposal_freeform_minProposerBalance,
NetworkParams.spam_protection_proposal_min_tokens,
]);
const { VEGA_DOCS_URL, VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
handleSubmit,
formState: { isSubmitting, errors },
} = useForm<FreeformProposalFormFields>();
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
const onSubmit = async (fields: FreeformProposalFormFields) => {
await submit({
rationale: {
title: fields.proposalTitle,
description: fields.proposalDescription,
},
terms: {
newFreeform: {},
closingTimestamp: getClosingTimestamp(fields.proposalVoteDeadline),
} as ProposalFreeformTerms,
});
};
return (
<AsyncRenderer loading={loading} error={error} data={params}>
<Heading title={t('NewFreeformProposal')} />
<VegaWalletContainer>
{() => (
<>
<ProposalFormMinRequirements
minProposerBalance={
params.governance_proposal_freeform_minProposerBalance
}
spamProtectionMin={params.spam_protection_proposal_min_tokens}
/>
{VEGA_DOCS_URL && (
<div className="text-sm">
<ProposalDocsLink
urlPart1={VEGA_DOCS_URL}
urlPart2={'/tutorials/proposals/freeform-proposal'}
/>
</div>
)}
{VEGA_EXPLORER_URL && (
<p className="text-sm">
{t('MoreProposalsInfo')}{' '}
<Link
href={`${VEGA_EXPLORER_URL}/governance`}
target="_blank"
>{`${VEGA_EXPLORER_URL}/governance`}</Link>
</p>
)}
<div data-testid="freeform-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}
/>
<ProposalFormVoteAndEnactmentDeadline
voteRegister={register('proposalVoteDeadline', {
required: t('Required'),
})}
voteErrorMessage={errors?.proposalVoteDeadline?.message}
voteMinClose={params.governance_proposal_freeform_minClose}
voteMaxClose={params.governance_proposal_freeform_maxClose}
/>
<ProposalFormSubmit isSubmitting={isSubmitting} />
<ProposalFormTransactionDialog
finalizedProposal={finalizedProposal}
TransactionDialog={Dialog}
/>
</form>
</div>
</>
)}
</VegaWalletContainer>
</AsyncRenderer>
);
};
@@ -0,0 +1,4 @@
export {
ProposeNetworkParameter,
ProposeNetworkParameter as default,
} from './propose-network-parameter';
@@ -0,0 +1,115 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { ProposeNetworkParameter } from './propose-network-parameter';
import { MockedProvider } from '@apollo/client/testing';
import {
mockWalletContext,
networkParamsQueryMock,
} from '../../test-helpers/mocks';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { BrowserRouter as Router } from 'react-router-dom';
jest.mock('../../../../hooks/use-network-param.tsx', () => ({
useNetworkParamWithKeys: () => ({
data: [
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateNetParam.maxClose',
value: '8760h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateNetParam.maxEnact',
value: '8760h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateNetParam.minClose',
value: '1h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateNetParam.minEnact',
value: '2h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateNetParam.minProposerBalance',
value: '1',
},
{
__typename: 'NetworkParameter',
key: 'spam.protection.proposal.min.tokens',
value: '1000000000000000000',
},
{
key: 'limits.assets.proposeEnabledFrom',
value: '',
},
{
key: 'limits.markets.proposeEnabledFrom',
value: '',
},
{
key: 'market.auction.maximumDuration',
value: '168h0m0s',
},
],
loading: false,
error: undefined,
}),
}));
const renderComponent = () =>
render(
<Router>
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposeNetworkParameter />
</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 Network Parameter', () => {
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy();
});
it('should render the correct title', () => {
renderComponent();
expect(screen.getByText('Update network parameter proposal')).toBeTruthy();
});
it('should render the network param select element with no initial value', () => {
renderComponent();
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', () => {
renderComponent();
const select = screen.getByTestId('proposal-parameter-select');
expect(select).toHaveValue('');
fireEvent.change(screen.getByTestId('proposal-parameter-select'), {
target: {
value: 'spam.protection.proposal.min.tokens',
},
});
expect(select).toHaveValue('spam.protection.proposal.min.tokens');
expect(
screen.getByTestId('selected-proposal-param-current-value')
).toHaveValue('1000000000000000000');
expect(
screen.getByTestId('selected-proposal-param-new-value')
).toBeVisible();
});
});
@@ -0,0 +1,275 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form';
import { isJsonObject, useNetworkParams } from '@vegaprotocol/react-helpers';
import {
useProposalSubmit,
getClosingTimestamp,
getEnactmentTimestamp,
} from '@vegaprotocol/governance';
import { useEnvironment } from '@vegaprotocol/environment';
import {
ProposalFormSubheader,
ProposalFormMinRequirements,
ProposalFormTitle,
ProposalFormDescription,
ProposalFormSubmit,
ProposalFormTransactionDialog,
ProposalFormVoteAndEnactmentDeadline,
ProposalDocsLink,
} from '../../components/propose';
import {
AsyncRenderer,
FormGroup,
Input,
InputError,
Link,
Select,
SyntaxHighlighter,
TextArea,
} from '@vegaprotocol/ui-toolkit';
import { Heading } from '../../../../components/heading';
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
interface SelectedNetworkParamCurrentValueProps {
value: string;
}
const SelectedNetworkParamCurrentValue = ({
value,
}: SelectedNetworkParamCurrentValueProps) => {
const { t } = useTranslation();
return (
<div className="mb-4">
<p className="text-sm text-white">{t('CurrentValue')}</p>
{isJsonObject(value) ? (
<SyntaxHighlighter data={JSON.parse(value)} />
) : (
<Input
value={value}
data-testid="selected-proposal-param-current-value"
readOnly
/>
)}
</div>
);
};
export interface NetworkParameterProposalFormFields {
proposalVoteDeadline: string;
proposalEnactmentDeadline: string;
proposalTitle: string;
proposalDescription: string;
proposalNetworkParameterKey: string;
proposalNetworkParameterValue: string;
proposalReference: string;
}
export const ProposeNetworkParameter = () => {
const [selectedNetworkParam, setSelectedNetworkParam] = useState<
string | undefined
>(undefined);
const {
params,
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams();
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
handleSubmit,
formState: { isSubmitting, errors },
} = useForm<NetworkParameterProposalFormFields>();
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
const selectedParamEntry = params
? Object.entries(params).find(([key]) => key === selectedNetworkParam)
: null;
const onSubmit = async (fields: NetworkParameterProposalFormFields) => {
const acutalNetworkParamKey = fields.proposalNetworkParameterKey
.split('_')
.join('.');
await submit({
rationale: {
title: fields.proposalTitle,
description: fields.proposalDescription,
},
terms: {
updateNetworkParameter: {
changes: {
key: acutalNetworkParamKey,
value: fields.proposalNetworkParameterValue,
},
},
closingTimestamp: getClosingTimestamp(fields.proposalVoteDeadline),
enactmentTimestamp: getEnactmentTimestamp(
fields.proposalVoteDeadline,
fields.proposalEnactmentDeadline
),
},
});
};
return (
<AsyncRenderer
loading={networkParamsLoading}
error={networkParamsError}
data={params}
>
<Heading title={t('NetworkParameterProposal')} />
<VegaWalletContainer>
{() => (
<>
<ProposalFormMinRequirements
minProposerBalance={
params.governance_proposal_updateNetParam_minProposerBalance
}
spamProtectionMin={params.spam_protection_proposal_min_tokens}
/>
{VEGA_DOCS_URL && (
<div className="text-sm">
<ProposalDocsLink
urlPart1={VEGA_DOCS_URL}
urlPart2={'/tutorials/proposals/network-parameter-proposal'}
/>
</div>
)}
{VEGA_EXPLORER_URL && (
<p className="text-sm">
{t('MoreNetParamsInfo')}{' '}
<Link
href={`${VEGA_EXPLORER_URL}/network-parameters`}
target="_blank"
>{`${VEGA_EXPLORER_URL}/network-parameters`}</Link>
</p>
)}
<div data-testid="network-parameter-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('SelectAParameterToChange')}
</ProposalFormSubheader>
<FormGroup
label={t('SelectAParameterToChange')}
labelFor="proposal-parameter-key"
hideLabel={true}
>
<Select
data-testid="proposal-parameter-select"
id="proposal-parameter-key"
{...register('proposalNetworkParameterKey', {
required: t('Required'),
})}
onChange={(e) => setSelectedNetworkParam(e.target.value)}
value={selectedNetworkParam}
>
<option value="">{t('SelectParameter')}</option>
{Object.keys(params).map((key) => {
const actualKey = key.split('_').join('.');
return (
<option key={key} value={key}>
{actualKey}
</option>
);
})}
</Select>
{errors?.proposalNetworkParameterKey?.message && (
<InputError intent="danger">
{errors?.proposalNetworkParameterKey?.message}
</InputError>
)}
</FormGroup>
{selectedNetworkParam && (
<div className="mt-[-10px]">
{selectedParamEntry && (
<SelectedNetworkParamCurrentValue
value={selectedParamEntry[1]}
/>
)}
<FormGroup
label={t('NewProposedValue')}
labelFor="proposal-parameter-new-value"
>
<TextArea
data-testid="selected-proposal-param-new-value"
id="proposal-parameter-new-value"
{...register('proposalNetworkParameterValue', {
required: t('Required'),
})}
/>
{errors?.proposalNetworkParameterValue?.message && (
<InputError intent="danger">
{errors?.proposalNetworkParameterValue?.message}
</InputError>
)}
</FormGroup>
</div>
)}
<ProposalFormVoteAndEnactmentDeadline
voteRegister={register('proposalVoteDeadline', {
required: t('Required'),
})}
voteErrorMessage={errors?.proposalVoteDeadline?.message}
voteMinClose={
params.governance_proposal_updateNetParam_minClose
}
voteMaxClose={
params.governance_proposal_updateNetParam_maxClose
}
enactmentRegister={register('proposalEnactmentDeadline', {
required: t('Required'),
})}
enactmentErrorMessage={
errors?.proposalEnactmentDeadline?.message
}
enactmentMinClose={
params.governance_proposal_updateNetParam_minEnact
}
enactmentMaxClose={
params.governance_proposal_updateNetParam_maxEnact
}
/>
<ProposalFormSubmit isSubmitting={isSubmitting} />
<ProposalFormTransactionDialog
finalizedProposal={finalizedProposal}
TransactionDialog={Dialog}
/>
</form>
</div>
</>
)}
</VegaWalletContainer>
</AsyncRenderer>
);
};
@@ -0,0 +1,4 @@
export {
ProposeNewAsset,
ProposeNewAsset as default,
} from './propose-new-asset';
@@ -0,0 +1,77 @@
import { MockedProvider } from '@apollo/client/testing';
import { BrowserRouter 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,
networkParamsQueryMock,
} from '../../test-helpers/mocks';
import { ProposeNewAsset } from './propose-new-asset';
jest.mock('../../../../hooks/use-network-param.tsx', () => ({
useNetworkParamWithKeys: () => ({
data: [
{
__typename: 'NetworkParameter',
key: 'governance.proposal.asset.maxClose',
value: '8760h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.asset.maxEnact',
value: '8760h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.asset.minClose',
value: '1h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.asset.minEnact',
value: '2h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.asset.minProposerBalance',
value: '1',
},
{
__typename: 'NetworkParameter',
key: 'spam.protection.proposal.min.tokens',
value: '1000000000000000000',
},
],
loading: false,
error: undefined,
}),
}));
const renderComponent = () =>
render(
<Router>
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposeNewAsset />
</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 New Asset', () => {
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy();
});
it('should render the title', () => {
renderComponent();
expect(screen.getByText('New asset proposal')).toBeTruthy();
});
});
@@ -0,0 +1,193 @@
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
useProposalSubmit,
getClosingTimestamp,
getEnactmentTimestamp,
getValidationTimestamp,
} from '@vegaprotocol/governance';
import { useEnvironment } from '@vegaprotocol/environment';
import {
ProposalFormMinRequirements,
ProposalFormTitle,
ProposalFormDescription,
ProposalFormTerms,
ProposalFormSubmit,
ProposalFormTransactionDialog,
ProposalFormSubheader,
ProposalFormVoteAndEnactmentDeadline,
ProposalDocsLink,
} from '../../components/propose';
import { AsyncRenderer, Link } from '@vegaprotocol/ui-toolkit';
import { Heading } from '../../../../components/heading';
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
import type { ProposalNewAssetTerms } from '@vegaprotocol/wallet';
import { NetworkParams, useNetworkParams } from '@vegaprotocol/react-helpers';
export interface NewAssetProposalFormFields {
proposalVoteDeadline: string;
proposalEnactmentDeadline: string;
proposalValidationDeadline: string;
proposalTitle: string;
proposalDescription: string;
proposalTerms: string;
proposalReference: string;
}
export const ProposeNewAsset = () => {
const {
params,
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams([
NetworkParams.governance_proposal_asset_minClose,
NetworkParams.governance_proposal_asset_maxClose,
NetworkParams.governance_proposal_asset_minEnact,
NetworkParams.governance_proposal_asset_maxEnact,
NetworkParams.governance_proposal_asset_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<NewAssetProposalFormFields>();
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
const onSubmit = async (fields: NewAssetProposalFormFields) => {
await submit({
rationale: {
title: fields.proposalTitle,
description: fields.proposalDescription,
},
terms: {
newAsset: {
...JSON.parse(fields.proposalTerms),
},
closingTimestamp: getClosingTimestamp(fields.proposalVoteDeadline),
enactmentTimestamp: getEnactmentTimestamp(
fields.proposalVoteDeadline,
fields.proposalEnactmentDeadline
),
validationTimestamp: getValidationTimestamp(
fields.proposalValidationDeadline
),
} as ProposalNewAssetTerms,
});
};
return (
<AsyncRenderer
loading={networkParamsLoading}
error={networkParamsError}
data={params}
>
<Heading title={t('NewAssetProposal')} />
<VegaWalletContainer>
{() => (
<>
<ProposalFormMinRequirements
minProposerBalance={
params.governance_proposal_asset_minProposerBalance
}
spamProtectionMin={params.spam_protection_proposal_min_tokens}
/>
{VEGA_DOCS_URL && (
<div className="text-sm">
<ProposalDocsLink
urlPart1={VEGA_DOCS_URL}
urlPart2={'/tutorials/proposals/new-asset-proposal'}
/>
</div>
)}
{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="new-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('NewAsset')}</ProposalFormSubheader>
<ProposalFormTerms
registerField={register('proposalTerms', {
required: t('Required'),
validate: {
validateJson: (value) => {
try {
JSON.parse(value);
return true;
} catch (e) {
return t('Must be valid JSON');
}
},
},
})}
labelOverride={'Terms.newAsset (JSON format)'}
errorMessage={errors?.proposalTerms?.message}
/>
<ProposalFormVoteAndEnactmentDeadline
voteRegister={register('proposalVoteDeadline', {
required: t('Required'),
})}
voteErrorMessage={errors?.proposalVoteDeadline?.message}
voteMinClose={params.governance_proposal_asset_minClose}
voteMaxClose={params.governance_proposal_asset_maxClose}
enactmentRegister={register('proposalEnactmentDeadline', {
required: t('Required'),
})}
enactmentErrorMessage={
errors?.proposalEnactmentDeadline?.message
}
enactmentMinClose={params.governance_proposal_asset_minEnact}
enactmentMaxClose={params.governance_proposal_asset_maxEnact}
validationRequired={true}
validationRegister={register('proposalValidationDeadline', {
required: t('Required'),
})}
validationErrorMessage={
errors?.proposalValidationDeadline?.message
}
/>
<ProposalFormSubmit isSubmitting={isSubmitting} />
<ProposalFormTransactionDialog
finalizedProposal={finalizedProposal}
TransactionDialog={Dialog}
/>
</form>
</div>
</>
)}
</VegaWalletContainer>
</AsyncRenderer>
);
};
@@ -0,0 +1,4 @@
export {
ProposeNewMarket,
ProposeNewMarket as default,
} from './propose-new-market';
@@ -0,0 +1,33 @@
import { render } from '@testing-library/react';
import { ProposeNewMarket } from './propose-new-market';
import { MockedProvider } from '@apollo/client/testing';
import {
mockWalletContext,
networkParamsQueryMock,
} from '../../test-helpers/mocks';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { BrowserRouter as Router } from 'react-router-dom';
const renderComponent = () =>
render(
<Router>
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposeNewMarket />
</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('ProposeNewMarket', () => {
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy();
});
});
@@ -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 {
ProposalFormMinRequirements,
ProposalFormTitle,
ProposalFormDescription,
ProposalFormTerms,
ProposalFormSubmit,
ProposalFormTransactionDialog,
ProposalFormSubheader,
ProposalFormVoteAndEnactmentDeadline,
ProposalDocsLink,
} from '../../components/propose';
import { AsyncRenderer, Link } from '@vegaprotocol/ui-toolkit';
import { Heading } from '../../../../components/heading';
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
import type { ProposalNewMarketTerms } from '@vegaprotocol/wallet';
import { NetworkParams, useNetworkParams } from '@vegaprotocol/react-helpers';
export interface NewMarketProposalFormFields {
proposalVoteDeadline: string;
proposalEnactmentDeadline: string;
proposalTitle: string;
proposalDescription: string;
proposalTerms: string;
proposalReference: string;
}
export const ProposeNewMarket = () => {
const {
params,
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams([
NetworkParams.governance_proposal_market_maxClose,
NetworkParams.governance_proposal_market_minClose,
NetworkParams.governance_proposal_market_maxEnact,
NetworkParams.governance_proposal_market_minEnact,
NetworkParams.governance_proposal_market_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<NewMarketProposalFormFields>();
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
const onSubmit = async (fields: NewMarketProposalFormFields) => {
await submit({
rationale: {
title: fields.proposalTitle,
description: fields.proposalDescription,
},
terms: {
newMarket: {
...JSON.parse(fields.proposalTerms),
},
closingTimestamp: getClosingTimestamp(fields.proposalVoteDeadline),
enactmentTimestamp: getEnactmentTimestamp(
fields.proposalVoteDeadline,
fields.proposalEnactmentDeadline
),
} as ProposalNewMarketTerms,
});
};
return (
<AsyncRenderer
loading={networkParamsLoading}
error={networkParamsError}
data={params}
>
<Heading title={t('NewMarketProposal')} />
<VegaWalletContainer>
{() => (
<>
<ProposalFormMinRequirements
minProposerBalance={
params.governance_proposal_market_minProposerBalance
}
spamProtectionMin={params.spam_protection_proposal_min_tokens}
/>
{VEGA_DOCS_URL && (
<div className="text-sm">
<ProposalDocsLink
urlPart1={VEGA_DOCS_URL}
urlPart2={'/tutorials/proposals/new-market-proposal'}
/>
</div>
)}
{VEGA_EXPLORER_URL && (
<p className="text-sm">
{t('MoreMarketsInfo')}{' '}
<Link
href={`${VEGA_EXPLORER_URL}/markets`}
target="_blank"
>{`${VEGA_EXPLORER_URL}/markets`}</Link>
</p>
)}
<div data-testid="new-market-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('NewMarket')}</ProposalFormSubheader>
<ProposalFormTerms
registerField={register('proposalTerms', {
required: t('Required'),
validate: {
validateJson: (value) => {
try {
JSON.parse(value);
return true;
} catch (e) {
return t('Must be valid JSON');
}
},
},
})}
labelOverride={'Terms.newMarket (JSON format)'}
errorMessage={errors?.proposalTerms?.message}
/>
<ProposalFormVoteAndEnactmentDeadline
voteRegister={register('proposalVoteDeadline', {
required: t('Required'),
})}
voteErrorMessage={errors?.proposalVoteDeadline?.message}
voteMinClose={params.governance_proposal_market_minClose}
voteMaxClose={params.governance_proposal_market_maxClose}
enactmentRegister={register('proposalEnactmentDeadline', {
required: t('Required'),
})}
enactmentErrorMessage={
errors?.proposalEnactmentDeadline?.message
}
enactmentMinClose={params.governance_proposal_market_minEnact}
enactmentMaxClose={params.governance_proposal_market_maxEnact}
/>
<ProposalFormSubmit isSubmitting={isSubmitting} />
<ProposalFormTransactionDialog
finalizedProposal={finalizedProposal}
TransactionDialog={Dialog}
/>
</form>
</div>
</>
)}
</VegaWalletContainer>
</AsyncRenderer>
);
};
@@ -0,0 +1,30 @@
import { render, screen } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import { Propose } from './propose';
const renderComponent = () =>
render(
<Router>
<Propose />
</Router>
);
describe('Propose', () => {
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy();
});
it('should render the heading, proposal type question and options', () => {
renderComponent();
expect(
screen.getByText('What type of proposal would you like to make?')
).toBeTruthy();
expect(screen.getByText('Network parameter')).toBeTruthy();
expect(screen.getByText('New market')).toBeTruthy();
expect(screen.getByText('Update market')).toBeTruthy();
expect(screen.getByText('New asset')).toBeTruthy();
expect(screen.getByText('Freeform')).toBeTruthy();
expect(screen.getByText('Let me choose (raw proposal)')).toBeTruthy();
});
});
@@ -1,21 +1,85 @@
import Routes from '../../routes';
import classnames from 'classnames';
import { useTranslation } from 'react-i18next';
import { ProposalForm } from '@vegaprotocol/governance';
import { Link } from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment';
import { Heading } from '../../../components/heading';
import { VegaWalletContainer } from '../../../components/vega-wallet-container';
import { ProposalDocsLink } from '../components/propose';
export const Propose = () => {
const { VEGA_DOCS_URL, VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const linkStyles = classnames('block underline mb-2');
return (
<>
<Heading title={t('NewProposal')} />
<VegaWalletContainer>
{() => (
<>
<p>{t('MinProposalRequirements')}</p>
<ProposalForm />
</>
<section className="pb-6">
<Heading title={t('NewProposal')} />
{VEGA_DOCS_URL && <ProposalDocsLink urlPart1={VEGA_DOCS_URL} />}
{VEGA_EXPLORER_URL && (
<p>
{t('MoreProposalsInfo')}{' '}
<Link
href={`${VEGA_EXPLORER_URL}/governance`}
target="_blank"
>{`${VEGA_EXPLORER_URL}/governance`}</Link>
</p>
)}
</VegaWalletContainer>
</section>
<section>
<h2 className="text-h5">{t('ProposalTypeQuestion')}</h2>
<ul>
<li>
<Link
href={`${Routes.GOVERNANCE}/propose/network-parameter`}
className={linkStyles}
>
{t('NetworkParameter')}
</Link>
</li>
<li>
<Link
href={`${Routes.GOVERNANCE}/propose/new-market`}
className={linkStyles}
>
{t('NewMarket')}
</Link>
</li>
<li>
<Link
href={`${Routes.GOVERNANCE}/propose/update-market`}
className={linkStyles}
>
{t('UpdateMarket')}
</Link>
</li>
<li>
<Link
href={`${Routes.GOVERNANCE}/propose/new-asset`}
className={linkStyles}
>
{t('NewAsset')}
</Link>
</li>
<li>
<Link
href={`${Routes.GOVERNANCE}/propose/freeform`}
className={linkStyles}
>
{t('Freeform')}
</Link>
</li>
<li>
<Link
href={`${Routes.GOVERNANCE}/propose/raw`}
className={linkStyles}
>
{t('RawProposal')}
</Link>
</li>
</ul>
</section>
</>
);
};
@@ -0,0 +1 @@
export { ProposeRaw, ProposeRaw as default } from './propose-raw';
@@ -1,5 +1,6 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
import type { MockedResponse } from '@apollo/client/testing';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { MockedProvider } from '@apollo/client/testing';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
@@ -8,11 +9,11 @@ import {
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';
import { ProposeRaw } from './propose-raw';
import { PROPOSAL_EVENT_SUB } from '@vegaprotocol/governance';
import type { ProposalEvent } from '@vegaprotocol/governance';
describe('ProposalForm', () => {
describe('Raw proposal form', () => {
const pubkey = '0x123';
const mockProposalEvent: MockedResponse<ProposalEvent> = {
request: {
@@ -44,18 +45,20 @@ describe('ProposalForm', () => {
};
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>
<AppStateProvider>
<MockedProvider mocks={[mockProposalEvent]}>
<VegaWalletContext.Provider
value={
{
keypair: { pub: pubkey },
sendTx: mockSendTx,
} as unknown as VegaWalletContextShape
}
>
<ProposeRaw />
</VegaWalletContext.Provider>
</MockedProvider>
</AppStateProvider>
);
};
@@ -0,0 +1,104 @@
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useEnvironment } from '@vegaprotocol/environment';
import { Heading } from '../../../../components/heading';
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
import {
FormGroup,
InputError,
Link,
TextArea,
} from '@vegaprotocol/ui-toolkit';
import { useProposalSubmit } from '@vegaprotocol/governance';
import {
ProposalDocsLink,
ProposalFormSubmit,
ProposalFormTransactionDialog,
} from '../../components/propose';
export interface RawProposalFormFields {
rawProposalData: string;
}
export const ProposeRaw = () => {
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
handleSubmit,
formState: { isSubmitting, errors },
} = useForm<RawProposalFormFields>();
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
const hasError = Boolean(errors.rawProposalData?.message);
const onSubmit = async (fields: RawProposalFormFields) => {
await submit(JSON.parse(fields.rawProposalData));
};
return (
<>
<Heading title={t('NewRawProposal')} />
<VegaWalletContainer>
{() => (
<>
{VEGA_DOCS_URL && (
<div className="text-sm">
<ProposalDocsLink urlPart1={VEGA_DOCS_URL} />
</div>
)}
{VEGA_EXPLORER_URL && (
<p className="text-sm">
{t('MoreProposalsInfo')}{' '}
<Link
href={`${VEGA_EXPLORER_URL}/governance`}
target="_blank"
>{`${VEGA_EXPLORER_URL}/governance`}</Link>
</p>
)}
<div data-testid="raw-proposal-form">
<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('rawProposalData', {
required: t('Required'),
validate: {
validateJson: (value) => {
try {
JSON.parse(value);
return true;
} catch (e) {
return t('Must be valid JSON');
}
},
},
})}
/>
{errors.rawProposalData?.message && (
<InputError intent="danger">
{errors.rawProposalData?.message}
</InputError>
)}
</FormGroup>
<ProposalFormSubmit isSubmitting={isSubmitting} />
<ProposalFormTransactionDialog
finalizedProposal={finalizedProposal}
TransactionDialog={Dialog}
/>
</form>
</div>
</>
)}
</VegaWalletContainer>
</>
);
};
@@ -0,0 +1,57 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: ProposalMarketsQuery
// ====================================================
export interface ProposalMarketsQuery_marketsConnection_edges_node_tradableInstrument_instrument {
__typename: "Instrument";
/**
* Full and fairly descriptive name for the instrument
*/
name: string;
/**
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
*/
code: string;
}
export interface ProposalMarketsQuery_marketsConnection_edges_node_tradableInstrument {
__typename: "TradableInstrument";
/**
* An instance of, or reference to, a fully specified instrument.
*/
instrument: ProposalMarketsQuery_marketsConnection_edges_node_tradableInstrument_instrument;
}
export interface ProposalMarketsQuery_marketsConnection_edges_node {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* An instance of, or reference to, a tradable instrument.
*/
tradableInstrument: ProposalMarketsQuery_marketsConnection_edges_node_tradableInstrument;
}
export interface ProposalMarketsQuery_marketsConnection_edges {
__typename: "MarketEdge";
node: ProposalMarketsQuery_marketsConnection_edges_node;
}
export interface ProposalMarketsQuery_marketsConnection {
__typename: "MarketConnection";
/**
* The markets in this connection
*/
edges: ProposalMarketsQuery_marketsConnection_edges[];
}
export interface ProposalMarketsQuery {
marketsConnection: ProposalMarketsQuery_marketsConnection;
}
@@ -0,0 +1,4 @@
export {
ProposeUpdateMarket,
ProposeUpdateMarket as default,
} from './propose-update-market';
@@ -0,0 +1,182 @@
import { MockedProvider } from '@apollo/client/testing';
import { BrowserRouter as Router } from 'react-router-dom';
import { render, screen, fireEvent } from '@testing-library/react';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import {
mockWalletContext,
networkParamsQueryMock,
} from '../../test-helpers/mocks';
import { ProposeUpdateMarket } from './propose-update-market';
jest.mock('../../../../hooks/use-network-param.tsx', () => ({
useNetworkParamWithKeys: () => ({
data: [
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateMarket.maxClose',
value: '8760h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateMarket.maxEnact',
value: '8760h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateMarket.minClose',
value: '1h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateMarket.minEnact',
value: '2h0m0s',
},
{
__typename: 'NetworkParameter',
key: 'governance.proposal.updateMarket.minProposerBalance',
value: '1',
},
{
__typename: 'NetworkParameter',
key: 'spam.protection.proposal.min.tokens',
value: '1000000000000000000',
},
],
loading: false,
error: undefined,
}),
}));
jest.mock('@apollo/client', () => ({
...jest.requireActual('@apollo/client'),
useQuery: jest.fn(() => ({
data: {
marketsConnection: {
__typename: 'MarketConnection',
edges: [
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: 'd2319c91104a523032cf04ac4e20962b87ee1f187d1e411a2cac12554dd38f29',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'ETHUSD (September Market)',
code: 'ETHiUSDT',
},
},
},
},
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: 'bb941f84e25b3a06068e33917d01215d56a51e9abff6ff9b9a3f2cf49b495e37',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'Las Vegas nuggets',
code: 'Nuggets2',
},
},
},
},
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: '976f71b8d6f99f3685297156a11245f21e49c99d3be17f2577a4bf693b5f37d1',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'Nuggets',
code: 'Nuggets',
},
},
},
},
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: '1cb2e1755208914b6f258a28babd19ae8dfbaf4084d8867d8a120c50dca1e17f',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'CELUSD (June 2022)',
code: 'CELUSD',
},
},
},
},
],
},
},
loading: false,
error: undefined,
})),
}));
const renderComponent = () =>
render(
<Router>
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposeUpdateMarket />
</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 Market', () => {
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy();
});
it('should render the title', () => {
renderComponent();
expect(screen.getByText('Update market proposal')).toBeTruthy();
});
it('should render the select element with no initial value', () => {
renderComponent();
expect(screen.getByTestId('proposal-market-select')).toHaveValue('');
});
it('should render the correct market details when the market select is used', () => {
renderComponent();
fireEvent.change(screen.getByTestId('proposal-market-select'), {
target: {
value:
'd2319c91104a523032cf04ac4e20962b87ee1f187d1e411a2cac12554dd38f29',
},
});
expect(screen.getByTestId('proposal-market-select')).toHaveValue(
'd2319c91104a523032cf04ac4e20962b87ee1f187d1e411a2cac12554dd38f29'
);
expect(screen.getByTestId('update-market-details')).toHaveTextContent(
'ETHUSD (September Market)'
);
expect(screen.getByTestId('update-market-details')).toHaveTextContent(
'ETHiUSDT'
);
expect(screen.getByTestId('update-market-details')).toHaveTextContent(
'd2319c91104a523032cf04ac4e20962b87ee1f187d1e411a2cac12554dd38f29'
);
});
});
@@ -0,0 +1,300 @@
import { gql, useQuery } from '@apollo/client';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form';
import {
useProposalSubmit,
getClosingTimestamp,
getEnactmentTimestamp,
} from '@vegaprotocol/governance';
import { useEnvironment } from '@vegaprotocol/environment';
import {
ProposalFormSubheader,
ProposalFormMinRequirements,
ProposalFormTitle,
ProposalFormDescription,
ProposalFormTerms,
ProposalFormSubmit,
ProposalFormTransactionDialog,
ProposalFormVoteAndEnactmentDeadline,
} from '../../components/propose';
import {
AsyncRenderer,
FormGroup,
InputError,
KeyValueTable,
KeyValueTableRow,
Link,
Select,
} from '@vegaprotocol/ui-toolkit';
import { Heading } from '../../../../components/heading';
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
import type { ProposalUpdateMarketTerms } from '@vegaprotocol/wallet';
import type { ProposalMarketsQuery } from './__generated__/ProposalMarketsQuery';
import { NetworkParams, useNetworkParams } from '@vegaprotocol/react-helpers';
const MARKETS_QUERY = gql`
query ProposalMarketsQuery {
marketsConnection {
edges {
node {
id
tradableInstrument {
instrument {
name
code
}
}
}
}
}
}
`;
export interface UpdateMarketProposalFormFields {
proposalVoteDeadline: string;
proposalEnactmentDeadline: string;
proposalTitle: string;
proposalDescription: string;
proposalMarketId: string;
proposalTerms: string;
proposalReference: string;
}
export const ProposeUpdateMarket = () => {
const {
params,
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams([
NetworkParams.governance_proposal_updateMarket_maxClose,
NetworkParams.governance_proposal_updateMarket_minClose,
NetworkParams.governance_proposal_updateMarket_maxEnact,
NetworkParams.governance_proposal_updateMarket_minEnact,
NetworkParams.governance_proposal_updateMarket_minProposerBalance,
NetworkParams.spam_protection_proposal_min_tokens,
]);
const {
data: marketsData,
loading: marketsLoading,
error: marketsError,
} = useQuery<ProposalMarketsQuery>(MARKETS_QUERY);
const sortedMarkets = useMemo(() => {
if (!marketsData) {
return [];
}
return marketsData.marketsConnection.edges
.map((edge) => edge.node)
.sort((a, b) => {
const aName = a.tradableInstrument.instrument.name;
const bName = b.tradableInstrument.instrument.name;
if (aName < bName) {
return -1;
}
if (aName > bName) {
return 1;
}
return 0;
});
}, [marketsData]);
const [selectedMarket, setSelectedMarket] = useState<string | undefined>(
undefined
);
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
handleSubmit,
formState: { isSubmitting, errors },
} = useForm<UpdateMarketProposalFormFields>();
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
const onSubmit = async (fields: UpdateMarketProposalFormFields) => {
await submit({
rationale: {
title: fields.proposalTitle,
description: fields.proposalDescription,
},
terms: {
updateMarket: {
marketId: fields.proposalMarketId,
changes: {
...JSON.parse(fields.proposalTerms),
},
},
closingTimestamp: getClosingTimestamp(fields.proposalVoteDeadline),
enactmentTimestamp: getEnactmentTimestamp(
fields.proposalVoteDeadline,
fields.proposalEnactmentDeadline
),
} as ProposalUpdateMarketTerms,
});
};
return (
<AsyncRenderer
loading={networkParamsLoading && marketsLoading}
error={networkParamsError && marketsError}
data={params && marketsData}
>
<Heading title={t('UpdateMarketProposal')} />
<VegaWalletContainer>
{() => (
<>
<ProposalFormMinRequirements
minProposerBalance={
params.governance_proposal_updateMarket_minProposerBalance
}
spamProtectionMin={params.spam_protection_proposal_min_tokens}
/>
{VEGA_EXPLORER_URL && (
<p className="text-sm">
{t('MoreMarketsInfo')}{' '}
<Link
href={`${VEGA_EXPLORER_URL}/markets`}
target="_blank"
>{`${VEGA_EXPLORER_URL}/markets`}</Link>
</p>
)}
<div data-testid="update-market-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('SelectAMarketToChange')}
</ProposalFormSubheader>
<FormGroup
label={t('SelectAMarketToChange')}
labelFor="proposal-market"
hideLabel={true}
>
<Select
data-testid="proposal-market-select"
id="proposal-market"
{...register('proposalMarketId', {
required: t('Required'),
})}
onChange={(e) => setSelectedMarket(e.target.value)}
>
<option value="">{t('SelectMarket')}</option>
{sortedMarkets.map((market) => (
<option value={market.id} key={market.id}>
{market.tradableInstrument.instrument.name}
</option>
))}
</Select>
{errors?.proposalMarketId?.message && (
<InputError intent="danger">
{errors?.proposalMarketId?.message}
</InputError>
)}
</FormGroup>
{selectedMarket && (
<div className="mt-[-20px] mb-6">
<KeyValueTable data-testid="update-market-details">
<KeyValueTableRow>
{t('MarketName')}
{
marketsData?.marketsConnection?.edges?.find(
({ node: market }) => market.id === selectedMarket
)?.node.tradableInstrument.instrument.name
}
</KeyValueTableRow>
<KeyValueTableRow>
{t('MarketCode')}
{
marketsData?.marketsConnection?.edges?.find(
({ node: market }) => market.id === selectedMarket
)?.node.tradableInstrument.instrument.code
}
</KeyValueTableRow>
<KeyValueTableRow>
{t('MarketId')}
{selectedMarket}
</KeyValueTableRow>
</KeyValueTable>
</div>
)}
<ProposalFormTerms
registerField={register('proposalTerms', {
required: t('Required'),
validate: {
validateJson: (value) => {
try {
JSON.parse(value);
return true;
} catch (e) {
return t('Must be valid JSON');
}
},
},
})}
labelOverride={t('ProposeUpdateMarketTerms')}
errorMessage={errors?.proposalTerms?.message}
/>
<ProposalFormVoteAndEnactmentDeadline
voteRegister={register('proposalVoteDeadline', {
required: t('Required'),
})}
voteErrorMessage={errors?.proposalVoteDeadline?.message}
voteMinClose={
params.governance_proposal_updateMarket_minClose
}
voteMaxClose={
params.governance_proposal_updateMarket_maxClose
}
enactmentRegister={register('proposalEnactmentDeadline', {
required: t('Required'),
})}
enactmentErrorMessage={
errors?.proposalEnactmentDeadline?.message
}
enactmentMinClose={
params.governance_proposal_updateMarket_minEnact
}
enactmentMaxClose={
params.governance_proposal_updateMarket_maxEnact
}
/>
<ProposalFormSubmit isSubmitting={isSubmitting} />
<ProposalFormTransactionDialog
finalizedProposal={finalizedProposal}
TransactionDialog={Dialog}
/>
</form>
</div>
</>
)}
</VegaWalletContainer>
</AsyncRenderer>
);
};
+12 -16
View File
@@ -10,7 +10,6 @@ import { useTranslation } from 'react-i18next';
import { EpochCountdown } from '../../../components/epoch-countdown';
import { Heading } from '../../../components/heading';
import { SplashLoader } from '../../../components/splash-loader';
import { NetworkParams } from '../../../config';
import {
AppStateActionType,
useAppState,
@@ -18,7 +17,7 @@ import {
import type { Rewards } from './__generated__/Rewards';
import { RewardInfo } from './reward-info';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useNetworkParams } from '@vegaprotocol/react-helpers';
import { useNetworkParams, NetworkParams } from '@vegaprotocol/react-helpers';
export const REWARDS_QUERY = gql`
query Rewards($partyId: ID!) {
@@ -73,33 +72,30 @@ export const RewardsIndex = () => {
variables: { partyId: keypair?.pub },
skip: !keypair?.pub,
});
const {
data: rewardAssetData,
loading: rewardAssetLoading,
error: rewardAssetError,
} = useNetworkParams([
NetworkParams.REWARD_ASSET,
NetworkParams.REWARD_PAYOUT_DURATION,
const { params } = useNetworkParams([
NetworkParams.reward_asset,
NetworkParams.reward_staking_delegation_payoutDelay,
]);
const payoutDuration = React.useMemo(() => {
if (!rewardAssetData || !rewardAssetData[1]) {
if (!params) {
return 0;
}
return new Duration(rewardAssetData[1]).milliseconds();
}, [rewardAssetData]);
return new Duration(
params.reward_staking_delegation_payoutDelay
).milliseconds();
}, [params]);
if (error || rewardAssetError) {
if (error) {
return (
<section>
<p>{t('Something went wrong')}</p>
{error && <pre>{error.message}</pre>}
{rewardAssetError && <pre>{rewardAssetError.message}</pre>}
</section>
);
}
if (loading || rewardAssetLoading || !rewardAssetData?.length) {
if (loading || !params) {
return (
<Splash>
<SplashLoader />
@@ -145,7 +141,7 @@ export const RewardsIndex = () => {
<RewardInfo
currVegaKey={keypair}
data={data}
rewardAssetId={rewardAssetData[0]}
rewardAssetId={params.reward_asset}
/>
) : (
<div>
+57
View File
@@ -129,6 +129,48 @@ const LazyGovernancePropose = React.lazy(
)
);
const LazyGovernanceProposeNetworkParameter = React.lazy(
() =>
import(
/* webpackChunkName: "route-governance-propose", webpackPrefetch: true */ './governance/propose/network-parameter'
)
);
const LazyGovernanceProposeNewMarket = React.lazy(
() =>
import(
/* webpackChunkName: "route-governance-propose", webpackPrefetch: true */ './governance/propose/new-market'
)
);
const LazyGovernanceProposeUpdateMarket = React.lazy(
() =>
import(
/* webpackChunkName: "route-governance-propose", webpackPrefetch: true */ './governance/propose/update-market'
)
);
const LazyGovernanceProposeNewAsset = React.lazy(
() =>
import(
/* webpackChunkName: "route-governance-propose", webpackPrefetch: true */ './governance/propose/new-asset'
)
);
const LazyGovernanceProposeFreeform = React.lazy(
() =>
import(
/* webpackChunkName: "route-governance-propose", webpackPrefetch: true */ './governance/propose/freeform'
)
);
const LazyGovernanceProposeRaw = React.lazy(
() =>
import(
/* webpackChunkName: "route-governance-propose", webpackPrefetch: true */ './governance/propose/raw'
)
);
const LazyRewards = React.lazy(
() =>
import(
@@ -220,6 +262,21 @@ const routerConfig = [
component: LazyGovernance,
children: [
{ path: ':proposalId', element: <LazyGovernanceProposal /> },
{
path: 'propose/network-parameter',
element: <LazyGovernanceProposeNetworkParameter />,
},
{
path: 'propose/new-market',
element: <LazyGovernanceProposeNewMarket />,
},
{
path: 'propose/update-market',
element: <LazyGovernanceProposeUpdateMarket />,
},
{ path: 'propose/new-asset', element: <LazyGovernanceProposeNewAsset /> },
{ path: 'propose/freeform', element: <LazyGovernanceProposeFreeform /> },
{ path: 'propose/raw', element: <LazyGovernanceProposeRaw /> },
{ path: 'propose', element: <LazyGovernancePropose /> },
{ path: 'rejected', element: <LazyRejectedGovernanceProposals /> },
{ index: true, element: <LazyGovernanceProposals /> },
@@ -6,6 +6,7 @@ import { MemoryRouter } from 'react-router-dom';
import { addDecimal } from '@vegaprotocol/react-helpers';
import type { Nodes_nodes } from './__generated__/Nodes';
import type { PartialDeep } from 'type-fest';
import { ValidatorStatus } from '@vegaprotocol/types';
jest.mock('../../components/epoch-countdown', () => ({
EpochCountdown: () => <div data-testid="epoch-info"></div>,
@@ -55,7 +56,7 @@ const MOCK_NODES = {
stakeScore: '0.2300971220240714',
performanceScore: '1',
votingPower: '2408',
status: 'tendermint',
status: ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
__typename: 'RankingScore',
},
}),
@@ -72,7 +73,7 @@ const MOCK_NODES = {
stakeScore: '0.0966762995515676',
performanceScore: '0.999629748500531',
votingPower: '1163',
status: 'tendermint',
status: ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
__typename: 'RankingScore',
},
}),
@@ -161,7 +162,7 @@ describe('Nodes list', () => {
stakeScore: '0.2300971220240714',
performanceScore: '1',
votingPower: '2408',
status: 'tendermint',
status: ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
__typename: 'RankingScore',
},
}),
@@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { TokenInput } from '../../components/token-input';
import { NetworkParams } from '../../config';
import { useAppState } from '../../contexts/app-state/app-state-context';
import { useSearchParams } from '../../hooks/use-search-params';
import { BigNumber } from '../../lib/bignumber';
@@ -30,7 +29,7 @@ import type {
UndelegateSubmissionBody,
} from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useNetworkParam } from '@vegaprotocol/react-helpers';
import { useNetworkParam, NetworkParams } from '@vegaprotocol/react-helpers';
export const PARTY_DELEGATIONS_QUERY = gql`
query PartyDelegations($partyId: ID!) {
@@ -103,14 +102,14 @@ export const StakingForm = ({
setAmount('');
}, [action, setAmount]);
const { data } = useNetworkParam(
NetworkParams.VALIDATOR_DELEGATION_MIN_AMOUNT
const { param: minAmount } = useNetworkParam(
NetworkParams.validators_delegation_minAmount
);
const minTokensWithDecimals = React.useMemo(() => {
const minTokens = new BigNumber(data && data.length === 1 ? data[0] : '');
const minTokens = new BigNumber(minAmount !== null ? minAmount : '');
return addDecimal(minTokens, appState.decimals);
}, [appState.decimals, data]);
}, [appState.decimals, minAmount]);
const maxDelegation = React.useMemo(() => {
if (action === Actions.Add) {
+8 -3
View File
@@ -55,11 +55,16 @@ describe('home', () => {
it('redirects to a the market list page if no sensible default is found', () => {
// Mock markets query that is triggered by home page to find default market
cy.mockGQL((req) => {
aliasQuery(req, 'MarketList', { markets: [] });
const data = {
marketsConnection: {
__typename: 'MarketConnection',
edges: [],
},
};
aliasQuery(req, 'Markets', data);
});
cy.visit('/');
cy.wait('@MarketList');
cy.wait('@Markets');
cy.url().should('eq', Cypress.config().baseUrl + '/markets');
});
});
+13 -5
View File
@@ -11,7 +11,9 @@ describe('markets table', () => {
it('renders markets correctly', () => {
cy.visit('/');
cy.wait('@Market');
cy.wait('@MarketList');
cy.wait('@Markets');
cy.wait('@MarketsDataQuery');
cy.wait('@MarketsCandlesQuery');
cy.get('[data-testid^="market-link-"]')
.should('not.be.empty')
.and('have.attr', 'href');
@@ -24,7 +26,9 @@ describe('markets table', () => {
it('renders market list drop down', () => {
cy.visit('/');
cy.wait('@MarketList');
cy.wait('@Markets');
cy.wait('@MarketsDataQuery');
cy.wait('@MarketsCandlesQuery');
openMarketDropDown();
cy.getByTestId('price').invoke('text').should('not.be.empty');
cy.getByTestId('trading-mode').should('not.be.empty');
@@ -35,7 +39,9 @@ describe('markets table', () => {
it('Able to select market from dropdown', () => {
cy.visit('/');
cy.wait('@MarketList');
cy.wait('@Markets');
cy.wait('@MarketsDataQuery');
cy.wait('@MarketsCandlesQuery');
openMarketDropDown();
cy.getByTestId('market-link-market-0').should('be.visible').click();
@@ -53,7 +59,9 @@ describe('markets table', () => {
'SOLUSD',
];
cy.visit('/');
cy.wait('@MarketList');
cy.wait('@Markets');
cy.wait('@MarketsDataQuery');
cy.wait('@MarketsCandlesQuery');
cy.getByTestId('link').should('have.attr', 'href', '/markets').click();
cy.url().should('eq', Cypress.config('baseUrl') + '/markets');
cy.contains('AAPL.MF21').should('be.visible');
@@ -127,7 +135,7 @@ describe('markets table', () => {
}
function verifyMarketSummaryDisplayed() {
const marketSummaryBlock = 'market-summary';
const marketSummaryBlock = 'header-summary';
const percentageValue = 'price-change-percentage';
const priceChangeValue = 'price-change';
const tradingVolume = 'trading-volume';
@@ -1,5 +1,4 @@
import merge from 'lodash/merge';
import { MarketTradingMode } from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries
import type { MarketDepth } from '../../../../../libs/market-depth/src/lib/__generated__/MarketDepth';
@@ -10,26 +9,10 @@ export const generateMarketDepth = (
const defaultResult: MarketDepth = {
market: {
id: 'market-0',
decimalPlaces: 5,
positionDecimalPlaces: 0,
data: {
marketTradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
staticMidPrice: '0',
indicativePrice: '0',
bestStaticBidPrice: '0',
bestStaticOfferPrice: '0',
indicativeVolume: '0',
market: {
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
__typename: 'Market',
},
__typename: 'MarketData',
},
depth: {
__typename: 'MarketDepth',
buy: [],
sell: [],
lastTrade: null,
sequenceNumber: '',
},
__typename: 'Market',
@@ -5,12 +5,17 @@ import {
MarketTradingMode,
} from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import type { MarketList, MarketList_markets } from '@vegaprotocol/market-list';
import type {
Markets,
Markets_marketsConnection_edges_node,
MarketsCandlesQuery,
MarketsCandlesQuery_marketsConnection_edges_node,
MarketsDataQuery,
MarketsDataQuery_marketsConnection_edges_node,
} from '@vegaprotocol/market-list';
export const generateMarkets = (
override?: PartialDeep<MarketList>
): MarketList => {
const markets: MarketList_markets[] = [
export const generateMarkets = (override?: PartialDeep<Markets>): Markets => {
const markets: Markets_marketsConnection_edges_node[] = [
{
id: 'market-0',
decimalPlaces: 5,
@@ -22,15 +27,6 @@ export const generateMarkets = (
close: '',
open: '',
},
candles: [
{
__typename: 'Candle',
open: '100',
close: '100',
high: '110',
low: '90',
},
],
fees: {
__typename: 'Fees',
factors: {
@@ -40,20 +36,6 @@ export const generateMarkets = (
liquidityFee: '',
},
},
data: {
market: {
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
state: MarketState.STATE_ACTIVE,
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
__typename: 'Market',
},
indicativeVolume: '0',
bestBidPrice: '0',
bestOfferPrice: '0',
markPrice: '4612690058',
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
__typename: 'MarketData',
},
tradableInstrument: {
instrument: {
id: '',
@@ -87,15 +69,6 @@ export const generateMarkets = (
close: '',
open: '',
},
candles: [
{
__typename: 'Candle',
open: '100',
close: '100',
high: '110',
low: '90',
},
],
fees: {
__typename: 'Fees',
factors: {
@@ -105,20 +78,6 @@ export const generateMarkets = (
liquidityFee: '',
},
},
data: {
market: {
id: '34d95e10faa00c21d19d382d6d7e6fc9722a96985369f0caec041b0f44b775ed',
state: MarketState.STATE_SUSPENDED,
tradingMode: MarketTradingMode.TRADING_MODE_NO_TRADING,
__typename: 'Market',
},
bestBidPrice: '0',
bestOfferPrice: '0',
indicativeVolume: '0',
markPrice: '8441',
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
__typename: 'MarketData',
},
tradableInstrument: {
instrument: {
id: 'SOLUSD',
@@ -152,15 +111,6 @@ export const generateMarkets = (
close: '2022-08-26T11:36:32.252490405Z',
open: null,
},
candles: [
{
__typename: 'Candle',
open: '100',
close: '100',
high: '110',
low: '90',
},
],
fees: {
__typename: 'Fees',
factors: {
@@ -170,20 +120,6 @@ export const generateMarkets = (
liquidityFee: '0.001',
},
},
data: {
market: {
id: 'a1c731af07570ca49b22a3cd253cc143dc14068edbec918e1087e69db934af5f',
state: MarketState.STATE_SUSPENDED,
tradingMode: MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
__typename: 'Market',
},
indicativeVolume: '0',
bestBidPrice: '0',
bestOfferPrice: '0',
markPrice: '4612690058',
trigger: AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY,
__typename: 'MarketData',
},
tradableInstrument: {
instrument: {
id: '',
@@ -217,15 +153,6 @@ export const generateMarkets = (
close: '2022-08-26T11:36:32.252490405Z',
open: null,
},
candles: [
{
__typename: 'Candle',
open: '100',
close: '100',
high: '110',
low: '90',
},
],
fees: {
__typename: 'Fees',
factors: {
@@ -235,20 +162,6 @@ export const generateMarkets = (
liquidityFee: '0.001',
},
},
data: {
market: {
id: 'bebea8ec669b913a7d6a704a6d8cede164bc1376229e0d472bc6fdaa976629b2',
state: MarketState.STATE_ACTIVE,
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
__typename: 'Market',
},
indicativeVolume: '0',
bestBidPrice: '0',
bestOfferPrice: '0',
markPrice: '4612690058',
trigger: AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY,
__typename: 'MarketData',
},
tradableInstrument: {
instrument: {
id: '',
@@ -272,8 +185,212 @@ export const generateMarkets = (
__typename: 'Market',
},
];
const defaultResult = {
markets,
const defaultResult: Markets = {
marketsConnection: {
__typename: 'MarketConnection',
edges: markets.map((node) => ({
__typename: 'MarketEdge',
node,
})),
},
};
return merge(defaultResult, override);
};
export const generateMarketsData = (
override?: PartialDeep<MarketsDataQuery>
): MarketsDataQuery => {
const markets: MarketsDataQuery_marketsConnection_edges_node[] = [
{
data: {
market: {
id: 'market-0',
__typename: 'Market',
},
marketTradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
staticMidPrice: '0',
indicativePrice: '0',
bestStaticBidPrice: '0',
bestStaticOfferPrice: '0',
indicativeVolume: '0',
bestBidPrice: '0',
bestOfferPrice: '0',
markPrice: '4612690058',
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
__typename: 'MarketData',
},
__typename: 'Market',
},
{
data: {
market: {
id: 'market-1',
__typename: 'Market',
},
marketTradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
staticMidPrice: '0',
indicativePrice: '0',
bestStaticBidPrice: '0',
bestStaticOfferPrice: '0',
indicativeVolume: '0',
bestBidPrice: '0',
bestOfferPrice: '0',
markPrice: '8441',
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
__typename: 'MarketData',
},
__typename: 'Market',
},
{
data: {
market: {
id: 'market-2',
__typename: 'Market',
},
marketTradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
staticMidPrice: '0',
indicativePrice: '0',
bestStaticBidPrice: '0',
bestStaticOfferPrice: '0',
indicativeVolume: '0',
bestBidPrice: '0',
bestOfferPrice: '0',
markPrice: '4612690058',
trigger: AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY,
__typename: 'MarketData',
},
__typename: 'Market',
},
{
data: {
market: {
id: 'market-3',
__typename: 'Market',
},
marketTradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
staticMidPrice: '0',
indicativePrice: '0',
bestStaticBidPrice: '0',
bestStaticOfferPrice: '0',
indicativeVolume: '0',
bestBidPrice: '0',
bestOfferPrice: '0',
markPrice: '4612690058',
trigger: AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY,
__typename: 'MarketData',
},
__typename: 'Market',
},
];
const defaultResult: MarketsDataQuery = {
marketsConnection: {
__typename: 'MarketConnection',
edges: markets.map((node) => ({
__typename: 'MarketEdge',
node,
})),
},
};
return merge(defaultResult, override);
};
export const generateMarketsCandles = (
override?: PartialDeep<MarketsCandlesQuery>
): MarketsCandlesQuery => {
const markets: MarketsCandlesQuery_marketsConnection_edges_node[] = [
{
__typename: 'Market',
id: 'market-0',
candlesConnection: {
__typename: 'CandleDataConnection',
edges: [
{
__typename: 'CandleEdge',
node: {
__typename: 'CandleNode',
open: '100',
close: '100',
high: '110',
low: '90',
volume: '1',
},
},
],
},
},
{
__typename: 'Market',
id: 'market-1',
candlesConnection: {
__typename: 'CandleDataConnection',
edges: [
{
__typename: 'CandleEdge',
node: {
__typename: 'CandleNode',
open: '100',
close: '100',
high: '110',
low: '90',
volume: '1',
},
},
],
},
},
{
__typename: 'Market',
id: 'market-2',
candlesConnection: {
__typename: 'CandleDataConnection',
edges: [
{
__typename: 'CandleEdge',
node: {
__typename: 'CandleNode',
open: '100',
close: '100',
high: '110',
low: '90',
volume: '1',
},
},
],
},
},
{
__typename: 'Market',
id: 'market-3',
candlesConnection: {
__typename: 'CandleDataConnection',
edges: [
{
__typename: 'CandleEdge',
node: {
__typename: 'CandleNode',
open: '100',
close: '100',
high: '110',
low: '90',
volume: '1',
},
},
],
},
},
];
const defaultResult: MarketsCandlesQuery = {
marketsConnection: {
__typename: 'MarketConnection',
edges: markets.map((node) => ({
__typename: 'MarketEdge',
node,
})),
},
};
return merge(defaultResult, override);
@@ -4,33 +4,13 @@ import type {
MarketDepth,
MarketDepth_market,
} from '@vegaprotocol/market-depth';
import { MarketTradingMode } from '@vegaprotocol/types';
export const generateOrderBook = (
override?: PartialDeep<MarketDepth>
): MarketDepth => {
const marketDepth: MarketDepth_market = {
id: 'b2426f67b085ba8fb429f1b529d49372b2d096c6fb6f509f76c5863abb6d969e',
decimalPlaces: 5,
positionDecimalPlaces: 0,
data: {
staticMidPrice: '826337',
marketTradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
indicativeVolume: '0',
indicativePrice: '0',
bestStaticBidPrice: '826336',
bestStaticOfferPrice: '826338',
market: {
id: 'b2426f67b085ba8fb429f1b529d49372b2d096c6fb6f509f76c5863abb6d969e',
__typename: 'Market',
},
__typename: 'MarketData',
},
depth: {
lastTrade: {
price: '826338',
__typename: 'Trade',
},
sell: [
{
price: '826338',
+9 -2
View File
@@ -8,7 +8,11 @@ import { generateDealTicketQuery } from './mocks/generate-deal-ticket-query';
import { generateMarket } from './mocks/generate-market';
import { generateMarketDepth } from './mocks/generate-market-depth';
import { generateMarketInfoQuery } from './mocks/generate-market-info-query';
import { generateMarkets } from './mocks/generate-markets';
import {
generateMarkets,
generateMarketsData,
generateMarketsCandles,
} from './mocks/generate-markets';
import { generateOrders } from './mocks/generate-orders';
import { generatePositions } from './mocks/generate-positions';
import { generateTrades } from './mocks/generate-trades';
@@ -31,7 +35,10 @@ export const mockTradingPage = (
},
})
);
aliasQuery(req, 'MarketList', generateMarkets());
aliasQuery(req, 'Markets', generateMarkets());
aliasQuery(req, 'MarketsDataQuery', generateMarketsData());
aliasQuery(req, 'MarketsCandlesQuery', generateMarketsCandles());
aliasQuery(req, 'MarketDepth', generateMarketDepth());
aliasQuery(req, 'Orders', generateOrders());
aliasQuery(req, 'Accounts', generateAccounts());
+1 -1
View File
@@ -1,6 +1,6 @@
# App configuration variables
NX_VEGA_ENV=TESTNET
NX_VEGA_URL=https://api.n09.testnet.vega.xyz/graphql
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"}
+1 -1
View File
@@ -1,6 +1,6 @@
export const Footer = () => {
return (
<footer className="px-4 py-2 text-xs border-t border-neutral-300 dark:border-neutral-600 bg-neutral-100 dark:bg-neutral-800">
<footer className="px-4 py-2 text-xs border-t border-default bg-neutral-100 dark:bg-neutral-800">
<div className="flex justify-between">
<div>Status</div>
</div>
+10 -9
View File
@@ -5,19 +5,20 @@ import { cloneElement } from 'react';
interface TradeMarketHeaderProps {
title: ReactNode;
children: ReactElement[];
children: Array<ReactElement | null>;
}
export const Header = ({ title, children }: TradeMarketHeaderProps) => {
return (
<header className="w-screen xl:px-4 pt-4 border-b border-neutral-300 dark:border-neutral-600">
<header className="w-screen xl:px-4 pt-4 border-b border-default">
<div className="xl:flex xl:gap-4 items-start">
<div className="px-4 mb-2">{title}</div>
<div className="mb-4 xl:mb-0">{title}</div>
<div
data-testid="market-summary"
data-testid="header-summary"
className="flex flex-nowrap items-start xl:flex-1 w-full overflow-x-auto text-xs "
>
{Children.map(children, (child, index) => {
if (!child) return null;
return cloneElement(child, {
id: `header-stat-${index}`,
});
@@ -40,17 +41,17 @@ export const HeaderStat = ({
description?: string | ReactNode;
}) => {
const itemClass =
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-neutral-300 dark:border-neutral-600';
const itemHeading = 'text-neutral-400';
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-default';
const itemHeading = 'text-neutral-500 dark:text-neutral-400';
return (
<div className={itemClass}>
<div id={id}>{heading}</div>
<Tooltip description={description}>
<div id={id} className={itemHeading}>
{heading}
<div aria-labelledby={id} className={itemHeading}>
{children}
</div>
</Tooltip>
<div aria-labelledby={id}>{children}</div>
</div>
);
};
+1 -1
View File
@@ -1,7 +1,7 @@
import classNames from 'classnames';
export function Vega({ className }: { className?: string }) {
const svgClasses = classNames(className, 'fill-white');
const svgClasses = classNames(className, 'fill-current');
return (
<svg
width="86"
+2 -2
View File
@@ -20,7 +20,7 @@ export const Navbar = ({ theme, toggleTheme }: NavbarProps) => {
}));
const tradingPath = marketId ? `/markets/${marketId}` : '/markets';
return (
<div className="px-4 flex items-stretch border-b border-neutral-300 dark:border-neutral-400 bg-black">
<div className="px-4 flex items-stretch border-b border-default bg-black text-white">
<div className="flex gap-4 mr-4 items-center h-full">
<Link href="/" passHref={true}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
@@ -28,7 +28,7 @@ export const Navbar = ({ theme, toggleTheme }: NavbarProps) => {
<Vega className="w-13" />
</a>
</Link>
<NetworkSwitcher fixedBg="dark" />
<NetworkSwitcher theme="dark" />
</div>
<nav className="flex items-center">
{[
@@ -30,7 +30,6 @@ export const VegaWalletConnectButton = ({
return (
<DropdownMenu open={dropdownOpen}>
<DropdownMenuTrigger
className="text-white hover:!bg-neutral-700"
data-testid="manage-vega-wallet"
onClick={() => setDropdownOpen((curr) => !curr)}
>
+6 -2
View File
@@ -1,4 +1,5 @@
import { useMarketList } from '@vegaprotocol/market-list';
import { activeMarketsProvider } from '@vegaprotocol/market-list';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
@@ -8,7 +9,10 @@ export function Index() {
const { replace } = useRouter();
// The default market selected in the platform behind the overlay
// should be the oldest market that is currently trading in continuous mode(i.e. not in auction).
const { data, error, loading } = useMarketList();
const { data, error, loading } = useDataProvider({
dataProvider: activeMarketsProvider,
noUpdate: true,
});
const { riskNoticeDialog, update } = useGlobalStore((store) => ({
riskNoticeDialog: store.riskNoticeDialog,
update: store.update,
+90 -126
View File
@@ -20,7 +20,6 @@ import {
ResizableGrid,
ResizableGridPanel,
ButtonLink,
Tooltip,
PriceCellChange,
Link,
} from '@vegaprotocol/ui-toolkit';
@@ -41,6 +40,7 @@ import {
} from '@vegaprotocol/types';
import { TradingModeTooltip } from '../../components/trading-mode-tooltip';
import { useRouter } from 'next/router';
import { Header, HeaderStat } from '../../components/header';
const TradingViews = {
Candles: CandlesChartContainer,
@@ -62,15 +62,16 @@ type ExpiryLabelProps = {
};
const ExpiryLabel = ({ market }: ExpiryLabelProps) => {
let content = null;
if (market.marketTimestamps.close === null) {
return <>{t('Not time-based')}</>;
content = t('Not time-based');
} else {
const closeDate = new Date(market.marketTimestamps.close);
const isExpired = Date.now() - closeDate.valueOf() > 0;
const expiryDate = getDateFormat().format(closeDate);
content = `${isExpired ? `${t('Expired')} ` : ''} ${expiryDate}`;
}
const closeDate = new Date(market.marketTimestamps.close);
const isExpired = Date.now() - closeDate.valueOf() > 0;
const expiryDate = getDateFormat().format(closeDate);
return <>{`${isExpired ? `${t('Expired')} ` : ''} ${expiryDate}`}</>;
return <div data-testid="trading-expiry">{content}</div>;
};
type ExpiryTooltipContentProps = {
@@ -115,6 +116,7 @@ export const TradeMarketHeader = ({
market,
onSelect,
}: TradeMarketHeaderProps) => {
const { push } = useRouter();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { setAssetDetailsDialogOpen, setAssetDetailsDialogSymbol } =
useAssetDetailsDialogStore();
@@ -122,123 +124,92 @@ export const TradeMarketHeader = ({
const candlesClose: string[] = (market?.candles || [])
.map((candle) => candle?.close)
.filter((c): c is CandleClose => c !== null);
const hasExpiry = market.marketTimestamps.close !== null;
const symbol =
market.tradableInstrument.instrument.product?.settlementAsset?.symbol;
const itemClass =
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-neutral-300 dark:border-neutral-600';
const itemHeading = 'text-neutral-500 dark:text-neutral-400';
const { push } = useRouter();
return (
<header className="w-screen px-4 border-b border-neutral-300 dark:border-neutral-600">
<div className="xl:flex xl:gap-4 items-start">
<div>
<SelectMarketPopover
marketName={market.tradableInstrument.instrument.name}
onSelect={onSelect}
<Header
title={
<SelectMarketPopover
marketName={market.tradableInstrument.instrument.name}
onSelect={onSelect}
/>
}
>
<HeaderStat
heading={t('Expiry')}
description={
<ExpiryTooltipContent
market={market}
explorerUrl={VEGA_EXPLORER_URL}
/>
}
>
<ExpiryLabel market={market} />
</HeaderStat>
<HeaderStat heading={t('Change (24h)')}>
<PriceCellChange
candles={candlesClose}
decimalPlaces={market.decimalPlaces}
/>
</HeaderStat>
<HeaderStat heading={t('Volume')}>
<div data-testid="trading-volume">
{market.data && market.data.indicativeVolume !== '0'
? addDecimalsFormatNumber(
market.data.indicativeVolume,
market.positionDecimalPlaces
)
: '-'}
</div>
<div
data-testid="market-summary"
className="flex flex-nowrap items-start mt-3 xl:flex-1 w-full overflow-x-auto text-xs "
>
<div className={itemClass}>
<div className={itemHeading}>{t('Expiry')}</div>
<Tooltip
align="start"
description={
<ExpiryTooltipContent
market={market}
explorerUrl={VEGA_EXPLORER_URL}
/>
}
>
<div
data-testid="trading-expiry"
className={classNames({
'underline decoration-dashed': !hasExpiry,
})}
>
<ExpiryLabel market={market} />
</div>
</Tooltip>
</div>
<div className={itemClass}>
<div className={itemHeading}>{t('Change (24h)')}</div>
<PriceCellChange
candles={candlesClose}
decimalPlaces={market.decimalPlaces}
/>
</div>
<div className={itemClass}>
<div className={itemHeading}>{t('Volume')}</div>
<div data-testid="trading-volume">
{market.data && market.data.indicativeVolume !== '0'
? addDecimalsFormatNumber(
market.data.indicativeVolume,
market.positionDecimalPlaces
)
: '-'}
</div>
</div>
<div className={itemClass}>
<div className={itemHeading}>{t('Trading mode')}</div>
<Tooltip
align="start"
description={
<TradingModeTooltip
market={market}
onSelect={(marketId: string) => {
onSelect(marketId);
push(`/liquidity/${marketId}`);
}}
/>
}
>
<div data-testid="trading-mode">
{market.tradingMode ===
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
market.data?.trigger &&
market.data.trigger !==
AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
? `${MarketTradingModeMapping[market.tradingMode]}
</HeaderStat>
<HeaderStat
heading={t('Trading mode')}
description={
<TradingModeTooltip
market={market}
onSelect={(marketId: string) => {
onSelect(marketId);
push(`/liquidity/${marketId}`);
}}
/>
}
>
<div data-testid="trading-mode">
{market.tradingMode ===
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
market.data?.trigger &&
market.data.trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
? `${MarketTradingModeMapping[market.tradingMode]}
- ${AuctionTriggerMapping[market.data.trigger]}`
: MarketTradingModeMapping[market.tradingMode]}
</div>
</Tooltip>
</div>
<div className={itemClass}>
<div className={itemHeading}>{t('Price')}</div>
<div data-testid="mark-price">
{market.data && market.data.markPrice !== '0'
? addDecimalsFormatNumber(
market.data.markPrice,
market.decimalPlaces
)
: '-'}
</div>
</div>
{symbol && (
<div className={itemClass}>
<div className={itemHeading}>{t('Settlement asset')}</div>
<div data-testid="trading-mode">
<ButtonLink
onClick={() => {
setAssetDetailsDialogOpen(true);
setAssetDetailsDialogSymbol(symbol);
}}
>
{symbol}
</ButtonLink>
</div>
</div>
)}
: MarketTradingModeMapping[market.tradingMode]}
</div>
</div>
</header>
</HeaderStat>
<HeaderStat heading={t('Price')}>
<div data-testid="mark-price">
{market.data && market.data.markPrice !== '0'
? addDecimalsFormatNumber(
market.data.markPrice,
market.decimalPlaces
)
: '-'}
</div>
</HeaderStat>
{symbol ? (
<HeaderStat heading={t('Settlement asset')}>
<div data-testid="trading-mode">
<ButtonLink
onClick={() => {
setAssetDetailsDialogOpen(true);
setAssetDetailsDialogSymbol(symbol);
}}
>
{symbol}
</ButtonLink>
</div>
</HeaderStat>
) : null}
</Header>
);
};
@@ -346,14 +317,7 @@ const TradeGridChild = ({ children }: TradeGridChildProps) => {
return (
<section className="h-full">
<AutoSizer>
{({ width, height }) => (
<div
style={{ width, height }}
className="overflow-auto border-[1px] dark:border-neutral-600"
>
{children}
</div>
)}
{({ width, height }) => <div style={{ width, height }}>{children}</div>}
</AutoSizer>
</section>
);
@@ -398,7 +362,7 @@ export const TradePanels = ({ market, onSelect }: TradePanelsProps) => {
)}
</AutoSizer>
</div>
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-neutral-300 dark:border-neutral-600">
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
{Object.keys(TradingViews).map((key) => {
const isActive = view === key;
const className = classNames('p-4 min-w-[100px] capitalize', {
+4
View File
@@ -18,3 +18,7 @@ html.dark {
--focus-border: theme('colors.vega.yellow');
--separator-border: theme('colors.neutral.600');
}
.border-default {
@apply border-neutral-300 dark:border-neutral-600;
}
@@ -64,6 +64,7 @@ export const DealTicketContainer = ({
)
) : (
<Splash>
{JSON.stringify(data)}
<p>{t('Could not load market')}</p>
</Splash>
)}
@@ -69,11 +69,7 @@ const NetworkLabel = ({
</span>
);
export const NetworkSwitcher = ({
fixedBg,
}: {
fixedBg?: 'dark' | 'light';
}) => {
export const NetworkSwitcher = ({ theme }: { theme?: 'dark' | 'light' }) => {
const { VEGA_ENV, VEGA_NETWORKS } = useEnvironment();
const [isOpen, setOpen] = useState(false);
const [isAdvancedView, setAdvancedView] = useState(false);
@@ -88,10 +84,9 @@ export const NetworkSwitcher = ({
[setOpen, setAdvancedView]
);
const dropdownTriggerClasses = classNames('hover:!bg-neutral-700', {
'dark:text-white dark:bg-black text-black bg-white': !fixedBg,
'text-black bg-white': fixedBg === 'light',
'text-white bg-black': fixedBg === 'dark',
const dropdownTriggerClasses = classNames({
'text-black hover:!bg-neutral-300': theme === 'light',
'text-white hover:!bg-neutral-700': theme === 'dark',
});
return (
@@ -72,6 +72,8 @@ const getBundledEnvironmentValue = (key: EnvKey) => {
return process.env['NX_VEGA_WALLET_URL'];
case 'VEGA_TOKEN_URL':
return process.env['NX_VEGA_TOKEN_URL'];
case 'VEGA_DOCS_URL':
return process.env['NX_VEGA_DOCS_URL'];
}
};
@@ -22,6 +22,7 @@ const schemaObject = {
VEGA_ENV: z.nativeEnum(Networks),
VEGA_EXPLORER_URL: z.optional(z.string()),
VEGA_TOKEN_URL: z.optional(z.string()),
VEGA_DOCS_URL: z.optional(z.string()),
VEGA_NETWORKS: z
.object(
Object.keys(Networks).reduce(
-1
View File
@@ -1,3 +1,2 @@
export * from './proposals-hooks';
export * from './proposal-form';
export * from './proposals-queries';
-84
View File
@@ -1,84 +0,0 @@
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, Dialog } = 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">
{errors.proposalData?.message}
</InputError>
)}
</FormGroup>
<Button
variant="primary"
type="submit"
data-testid="proposal-submit"
disabled={isSubmitting}
>
{isSubmitting ? t('Submitting') : t('Submit')} {t('Proposal')}
</Button>
<Dialog
title={getProposalDialogTitle(finalizedProposal?.state)}
intent={getProposalDialogIntent(finalizedProposal?.state)}
icon={getProposalDialogIcon(finalizedProposal?.state)}
>
{finalizedProposal?.rejectionReason ? (
<p>{finalizedProposal.rejectionReason}</p>
) : undefined}
</Dialog>
</form>
);
};
@@ -0,0 +1,24 @@
import { getClosingTimestamp } from './get-closing-timestamp';
import { addHours, addMinutes, getTime } from 'date-fns';
describe('getClosingTimestamp', () => {
it('should return the correct timestamp if the proposalVoteDeadline is 1 (when 2 mins are added)', () => {
const proposalVoteDeadline = 1;
const expected = Math.floor(
getTime(
addHours(addMinutes(new Date(Date.now()), 2), proposalVoteDeadline)
) / 1000
);
const actual = getClosingTimestamp(proposalVoteDeadline);
expect(actual).toEqual(expected);
});
it('should return the correct timestamp if the proposalVoteDeadline is 2 (when no extra mins are added)', () => {
const proposalVoteDeadline = 2;
const expected = Math.floor(
getTime(addHours(new Date(Date.now()), proposalVoteDeadline)) / 1000
);
const actual = getClosingTimestamp(proposalVoteDeadline);
expect(actual).toEqual(expected);
});
});
@@ -0,0 +1,17 @@
import { addHours, addMinutes, getTime } from 'date-fns';
// If proposaVoteDeadline is at its minimum of 1 hour, then we add
// 2 extra minutes to the closing timestamp to ensure that there's time
// to confirm in the wallet.
export const getClosingTimestamp = (proposalVoteDeadline: string) =>
Math.floor(
getTime(
proposalVoteDeadline === '1'
? addHours(
addMinutes(new Date(Date.now()), 2),
Number(proposalVoteDeadline)
)
: addHours(new Date(Date.now()), Number(proposalVoteDeadline))
) / 1000
);
@@ -0,0 +1,19 @@
import { getEnactmentTimestamp } from './get-enactment-timestamp';
import { addHours, getTime } from 'date-fns';
describe('getEnactmentTimestamp', () => {
it('should return the correct timestamp', () => {
const proposalVoteDeadline = 1;
const enactmentDeadline = 1;
const expected = Math.floor(
getTime(
addHours(new Date(Date.now()), proposalVoteDeadline + enactmentDeadline)
) / 1000
);
const actual = getEnactmentTimestamp(
proposalVoteDeadline,
enactmentDeadline
);
expect(actual).toEqual(expected);
});
});
@@ -0,0 +1,15 @@
import { addHours, fromUnixTime, getTime } from 'date-fns';
import { getClosingTimestamp } from './get-closing-timestamp';
export const getEnactmentTimestamp = (
proposalVoteDeadline: string,
enactmentDeadline: string
) =>
Math.floor(
getTime(
addHours(
new Date(fromUnixTime(getClosingTimestamp(proposalVoteDeadline))),
Number(enactmentDeadline)
)
) / 1000
);
@@ -0,0 +1,29 @@
// test getValidationTimestamp
import { addHours, addMinutes, getTime } from 'date-fns';
import { getValidationTimestamp } from './get-validation-timestamp';
describe('getValidationTimestamp', () => {
it('should return the correct timestamp if the proposalValidationDeadline is 0 (when 2 mins are added)', () => {
const proposalValidationDeadline = 0;
const expected = Math.floor(
getTime(
addHours(
addMinutes(new Date(Date.now()), 2),
proposalValidationDeadline
)
) / 1000
);
const actual = getValidationTimestamp(proposalValidationDeadline);
expect(actual).toEqual(expected);
});
it('should return the correct timestamp if the proposalValidationDeadline is 1 (when no extra mins are added)', () => {
const proposalValidationDeadline = 1;
const expected = Math.floor(
getTime(addHours(new Date(Date.now()), proposalValidationDeadline)) / 1000
);
const actual = getValidationTimestamp(proposalValidationDeadline);
expect(actual).toEqual(expected);
});
});
@@ -0,0 +1,17 @@
import { addHours, addMinutes, getTime } from 'date-fns';
// If proposalValidationDeadline is at its minimum of 0 hours, then we add
// 2 extra minutes to the validation timestamp to ensure that there's time
// to confirm in the wallet.
export const getValidationTimestamp = (proposalValidationDeadline: string) =>
Math.floor(
getTime(
proposalValidationDeadline === '0'
? addHours(
addMinutes(new Date(Date.now()), 2),
Number(proposalValidationDeadline)
)
: addHours(new Date(Date.now()), Number(proposalValidationDeadline))
) / 1000
);
+3
View File
@@ -1 +1,4 @@
export * from './proposal-dialog-helpers';
export * from './get-closing-timestamp';
export * from './get-enactment-timestamp';
export * from './get-validation-timestamp';

Some files were not shown because too many files have changed in this diff Show More