Compare commits
58
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb603ce6ea | ||
|
|
cc0e4987e5 | ||
|
|
a96bfeec8f | ||
|
|
1c5f44bb6c | ||
|
|
9b9cd22b63 | ||
|
|
35eb7543a7 | ||
|
|
1ef017e14c | ||
|
|
2eea3e2e34 | ||
|
|
a989d3a0b2 | ||
|
|
4f8a585956 | ||
|
|
e67f322400 | ||
|
|
2a700ddd84 | ||
|
|
238e019708 | ||
|
|
f701124f0f | ||
|
|
c5613f0107 | ||
|
|
040cf0b83b | ||
|
|
9d2f2fe3aa | ||
|
|
a08f63c7d8 | ||
|
|
3ff5bbb5a7 | ||
|
|
535bdb5183 | ||
|
|
4505e86363 | ||
|
|
7b2e86f32e | ||
|
|
a6ae5dd526 | ||
|
|
6d5aaa29ec | ||
|
|
9ff908cf4d | ||
|
|
bccfa05c89 | ||
|
|
01cb4fe74e | ||
|
|
0ccb70b68b | ||
|
|
55318f18bc | ||
|
|
22c2e49a98 | ||
|
|
48110180f2 | ||
|
|
bd3c27b9dc | ||
|
|
5a1b5b0efe | ||
|
|
c5fb26bd4c | ||
|
|
cb455e2399 | ||
|
|
81f11fbb6b | ||
|
|
aaecccc386 | ||
|
|
e3a0b9a3da | ||
|
|
7841c06121 | ||
|
|
15dfbee0d3 | ||
|
|
d2637a9645 | ||
|
|
7695ea567a | ||
|
|
a8a3753237 | ||
|
|
8cc499880c | ||
|
|
aedc4ba223 | ||
|
|
1579439730 | ||
|
|
8416d9a15e | ||
|
|
aa34dc6751 | ||
|
|
c39c111a1c | ||
|
|
2cdfb63050 | ||
|
|
52b1c24070 | ||
|
|
3a0a527805 | ||
|
|
b174d275c7 | ||
|
|
dc32655e02 | ||
|
|
7d4ac4648a | ||
|
|
d314b20a08 | ||
|
|
5925dc006f | ||
|
|
b05da4dceb |
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,3 +1,3 @@
|
||||
export * from './flags';
|
||||
export * from './links';
|
||||
export * from './network-params';
|
||||
export * from './env';
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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,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>
|
||||
);
|
||||
};
|
||||
+26
@@ -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>
|
||||
);
|
||||
};
|
||||
+32
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
+29
@@ -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>
|
||||
);
|
||||
};
|
||||
+18
@@ -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();
|
||||
});
|
||||
});
|
||||
+37
@@ -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>
|
||||
);
|
||||
+194
@@ -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);
|
||||
});
|
||||
});
|
||||
+380
@@ -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';
|
||||
+115
@@ -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();
|
||||
});
|
||||
});
|
||||
+275
@@ -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';
|
||||
+19
-16
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Generated
+57
@@ -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';
|
||||
+182
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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,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>
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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,3 +1,2 @@
|
||||
export * from './proposals-hooks';
|
||||
export * from './proposal-form';
|
||||
export * from './proposals-queries';
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
Reference in New Issue
Block a user