feat: view proposed market change (#2189)

This commit is contained in:
Art 2022-11-25 16:10:22 +01:00 committed by GitHub
parent 642bc8072b
commit 246c07f355
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1554 additions and 619 deletions

View File

@ -1,4 +1,9 @@
import { aliasQuery } from '@vegaprotocol/cypress';
import { Schema } from '@vegaprotocol/types';
import {
generateProposals,
marketUpdateProposal,
} from '../support/mocks/generate-proposals';
const marketSummaryBlock = 'header-summary';
const marketExpiry = 'market-expiry';
@ -12,6 +17,42 @@ const priceChangeValue = 'price-change';
const itemHeader = 'item-header';
const itemValue = 'item-value';
describe('Market proposal notification', { tags: '@smoke' }, () => {
before(() => {
cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
);
cy.mockGQL((req) => {
aliasQuery(
req,
'ProposalsList',
generateProposals([marketUpdateProposal])
);
});
cy.mockGQLSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@MarketData');
cy.getByTestId(marketSummaryBlock).should('be.visible');
});
it('should display market proposal notification if proposal found', () => {
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('market-proposal-notification').should(
'contain.text',
'Changes have been proposed for this market'
);
cy.getByTestId('market-proposal-notification').within(() => {
cy.getByTestId('external-link').should(
'have.attr',
'href',
'https://stagnet3.token.vega.xyz/governance/123'
);
});
});
});
});
describe('Market trading page', () => {
before(() => {
cy.mockTradingPage(

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,7 @@ import { Last24hPriceChange } from '../../components/last-24h-price-change';
import { MarketMarkPrice } from '../../components/market-mark-price';
import { MarketTradingModeComponent } from '../../components/market-trading-mode';
import { Last24hVolume } from '../../components/last-24h-volume';
import { MarketProposalNotification } from '@vegaprotocol/governance';
const NO_MARKET = t('No market');
@ -177,6 +178,7 @@ export const TradeMarketHeader = ({
</div>
</HeaderStat>
) : null}
<MarketProposalNotification marketId={market?.id} />
</Header>
);
};

View File

@ -10,7 +10,7 @@ interface TradeMarketHeaderProps {
export const Header = ({ title, children }: TradeMarketHeaderProps) => {
return (
<header className="w-screen xl:px-4 pt-4 border-b border-default">
<header className="w-screen xl:px-4 pt-3 border-b border-default">
<div className="xl:flex xl:gap-4 items-start">
<div className="mb-4 xl:mb-0 px-4 xl:px-0">{title}</div>
<div
@ -43,7 +43,7 @@ export const HeaderStat = ({
testId?: string;
}) => {
const itemClass =
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-default';
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-default mt-1';
const itemHeading = 'text-neutral-500 dark:text-neutral-400';
return (

View File

@ -144,7 +144,7 @@ export const SelectMarketPopover = ({
onCellClick: OnCellClickHandler;
}) => {
const triggerClasses =
'sm:text-lg md:text-xl lg:text-2xl flex items-center gap-2 whitespace-nowrap hover:text-neutral-500 dark:hover:text-neutral-300';
'sm:text-lg md:text-xl lg:text-2xl flex items-center gap-2 whitespace-nowrap hover:text-neutral-500 dark:hover:text-neutral-300 mt-1';
const { pubKey } = useVegaWallet();
const [open, setOpen] = useState(false);
const { data, loading: marketsLoading } = useMarketList();

View File

@ -3,6 +3,7 @@ import { useAssetsDataProvider } from './assets-data-provider';
import { Button, Dialog, Icon, Splash } from '@vegaprotocol/ui-toolkit';
import create from 'zustand';
import { AssetDetailsTable } from './asset-details-table';
import { AssetProposalNotification } from '@vegaprotocol/governance';
export type AssetDetailsDialogStore = {
isOpen: boolean;
@ -49,6 +50,7 @@ export const AssetDetailsDialog = ({
const content = asset ? (
<div className="my-2">
<AssetProposalNotification assetId={asset.id} />
<AssetDetailsTable asset={asset} />
</div>
) : (

View File

@ -0,0 +1,35 @@
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/react-helpers';
import { Schema } from '@vegaprotocol/types';
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
import { useUpdateProposal } from '../lib';
type AssetProposalNotificationProps = {
assetId?: string;
};
export const AssetProposalNotification = ({
assetId,
}: AssetProposalNotificationProps) => {
const tokenLink = useLinks(DApp.Token);
const { data: proposal } = useUpdateProposal({
id: assetId,
proposalType: Schema.ProposalType.TYPE_UPDATE_ASSET,
});
if (proposal) {
const proposalLink = tokenLink(
TOKEN_PROPOSAL.replace(':id', proposal.id || '')
);
return (
<Notification
intent={Intent.Warning}
message={t('Changes have been proposed for this asset')}
testId="asset-proposal-notification"
>
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
</Notification>
);
}
return null;
};

View File

@ -0,0 +1,2 @@
export * from './asset-proposal-notification';
export * from './market-proposal-notification';

View File

@ -0,0 +1,35 @@
import { t } from '@vegaprotocol/react-helpers';
import { Schema } from '@vegaprotocol/types';
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { useUpdateProposal } from '../lib';
type MarketProposalNotificationProps = {
marketId?: string;
};
export const MarketProposalNotification = ({
marketId,
}: MarketProposalNotificationProps) => {
const tokenLink = useLinks(DApp.Token);
const { data: proposal } = useUpdateProposal({
id: marketId,
proposalType: Schema.ProposalType.TYPE_UPDATE_MARKET,
});
if (proposal) {
const proposalLink = tokenLink(
TOKEN_PROPOSAL.replace(':id', proposal.id || '')
);
return (
<Notification
intent={Intent.Warning}
message={t('Changes have been proposed for this market')}
testId="market-proposal-notification"
>
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
</Notification>
);
}
return null;
};

View File

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

View File

@ -12,6 +12,63 @@ fragment NewMarketFields on NewMarket {
}
}
fragment UpdateMarketFields on UpdateMarket {
marketId
updateMarketConfiguration {
instrument {
code
product {
quoteName
}
}
priceMonitoringParameters {
triggers {
horizonSecs
probability
auctionExtensionSecs
}
}
liquidityMonitoringParameters {
targetStakeParameters {
timeWindow
scalingFactor
}
triggeringRatio
}
riskParameters {
__typename
... on UpdateMarketSimpleRiskModel {
simple {
factorLong
factorShort
}
}
... on UpdateMarketLogNormalRiskModel {
logNormal {
riskAversionParameter
tau
params {
mu
r
sigma
}
}
}
}
}
}
fragment UpdateAssetFields on UpdateAsset {
assetId
quantum
source {
... on UpdateERC20 {
lifetimeLimit
withdrawThreshold
}
}
}
fragment ProposalListFields on Proposal {
id
reference
@ -36,6 +93,12 @@ fragment ProposalListFields on Proposal {
... on NewMarket {
...NewMarketFields
}
... on UpdateMarket {
...UpdateMarketFields
}
... on UpdateAsset {
...UpdateAssetFields
}
}
}
}

View File

@ -5,7 +5,11 @@ import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type NewMarketFieldsFragment = { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', code: string, name: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', id: string, name: string, symbol: string } } | null } };
export type ProposalListFieldsFragment = { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: string, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalWeight: string } }, terms: { __typename?: 'ProposalTerms', closingDatetime: string, enactmentDatetime?: string | null, change: { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', code: string, name: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', id: string, name: string, symbol: string } } | null } } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateNetworkParameter' } } };
export type UpdateMarketFieldsFragment = { __typename?: 'UpdateMarket', marketId: string, updateMarketConfiguration: { __typename?: 'UpdateMarketConfiguration', instrument: { __typename?: 'UpdateInstrumentConfiguration', code: string, product: { __typename?: 'UpdateFutureProduct', quoteName: string } }, priceMonitoringParameters: { __typename?: 'PriceMonitoringParameters', triggers?: Array<{ __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number }> | null }, liquidityMonitoringParameters: { __typename?: 'LiquidityMonitoringParameters', triggeringRatio: number, targetStakeParameters: { __typename?: 'TargetStakeParameters', timeWindow: number, scalingFactor: number } }, riskParameters: { __typename: 'UpdateMarketLogNormalRiskModel', logNormal?: { __typename?: 'LogNormalRiskModel', riskAversionParameter: number, tau: number, params: { __typename?: 'LogNormalModelParams', mu: number, r: number, sigma: number } } | null } | { __typename: 'UpdateMarketSimpleRiskModel', simple?: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } | null } } };
export type UpdateAssetFieldsFragment = { __typename?: 'UpdateAsset', assetId: string, quantum: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } };
export type ProposalListFieldsFragment = { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: string, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalWeight: string } }, terms: { __typename?: 'ProposalTerms', closingDatetime: string, enactmentDatetime?: string | null, change: { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', code: string, name: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', id: string, name: string, symbol: string } } | null } } | { __typename?: 'UpdateAsset', assetId: string, quantum: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string, updateMarketConfiguration: { __typename?: 'UpdateMarketConfiguration', instrument: { __typename?: 'UpdateInstrumentConfiguration', code: string, product: { __typename?: 'UpdateFutureProduct', quoteName: string } }, priceMonitoringParameters: { __typename?: 'PriceMonitoringParameters', triggers?: Array<{ __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number }> | null }, liquidityMonitoringParameters: { __typename?: 'LiquidityMonitoringParameters', triggeringRatio: number, targetStakeParameters: { __typename?: 'TargetStakeParameters', timeWindow: number, scalingFactor: number } }, riskParameters: { __typename: 'UpdateMarketLogNormalRiskModel', logNormal?: { __typename?: 'LogNormalRiskModel', riskAversionParameter: number, tau: number, params: { __typename?: 'LogNormalModelParams', mu: number, r: number, sigma: number } } | null } | { __typename: 'UpdateMarketSimpleRiskModel', simple?: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } | null } } } | { __typename?: 'UpdateNetworkParameter' } } };
export type ProposalsListQueryVariables = Types.Exact<{
proposalType?: Types.InputMaybe<Types.ProposalType>;
@ -13,7 +17,7 @@ export type ProposalsListQueryVariables = Types.Exact<{
}>;
export type ProposalsListQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: string, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalWeight: string } }, terms: { __typename?: 'ProposalTerms', closingDatetime: string, enactmentDatetime?: string | null, change: { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', code: string, name: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', id: string, name: string, symbol: string } } | null } } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateNetworkParameter' } } } } | null> | null } | null };
export type ProposalsListQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: string, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalWeight: string } }, terms: { __typename?: 'ProposalTerms', closingDatetime: string, enactmentDatetime?: string | null, change: { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', code: string, name: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', id: string, name: string, symbol: string } } | null } } | { __typename?: 'UpdateAsset', assetId: string, quantum: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string, updateMarketConfiguration: { __typename?: 'UpdateMarketConfiguration', instrument: { __typename?: 'UpdateInstrumentConfiguration', code: string, product: { __typename?: 'UpdateFutureProduct', quoteName: string } }, priceMonitoringParameters: { __typename?: 'PriceMonitoringParameters', triggers?: Array<{ __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number }> | null }, liquidityMonitoringParameters: { __typename?: 'LiquidityMonitoringParameters', triggeringRatio: number, targetStakeParameters: { __typename?: 'TargetStakeParameters', timeWindow: number, scalingFactor: number } }, riskParameters: { __typename: 'UpdateMarketLogNormalRiskModel', logNormal?: { __typename?: 'LogNormalRiskModel', riskAversionParameter: number, tau: number, params: { __typename?: 'LogNormalModelParams', mu: number, r: number, sigma: number } } | null } | { __typename: 'UpdateMarketSimpleRiskModel', simple?: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } | null } } } | { __typename?: 'UpdateNetworkParameter' } } } } | null> | null } | null };
export const NewMarketFieldsFragmentDoc = gql`
fragment NewMarketFields on NewMarket {
@ -30,6 +34,65 @@ export const NewMarketFieldsFragmentDoc = gql`
}
}
`;
export const UpdateMarketFieldsFragmentDoc = gql`
fragment UpdateMarketFields on UpdateMarket {
marketId
updateMarketConfiguration {
instrument {
code
product {
quoteName
}
}
priceMonitoringParameters {
triggers {
horizonSecs
probability
auctionExtensionSecs
}
}
liquidityMonitoringParameters {
targetStakeParameters {
timeWindow
scalingFactor
}
triggeringRatio
}
riskParameters {
__typename
... on UpdateMarketSimpleRiskModel {
simple {
factorLong
factorShort
}
}
... on UpdateMarketLogNormalRiskModel {
logNormal {
riskAversionParameter
tau
params {
mu
r
sigma
}
}
}
}
}
}
`;
export const UpdateAssetFieldsFragmentDoc = gql`
fragment UpdateAssetFields on UpdateAsset {
assetId
quantum
source {
... on UpdateERC20 {
lifetimeLimit
withdrawThreshold
}
}
}
`;
export const ProposalListFieldsFragmentDoc = gql`
fragment ProposalListFields on Proposal {
id
@ -55,10 +118,18 @@ export const ProposalListFieldsFragmentDoc = gql`
... on NewMarket {
...NewMarketFields
}
... on UpdateMarket {
...UpdateMarketFields
}
... on UpdateAsset {
...UpdateAssetFields
}
}
}
}
${NewMarketFieldsFragmentDoc}`;
${NewMarketFieldsFragmentDoc}
${UpdateMarketFieldsFragmentDoc}
${UpdateAssetFieldsFragmentDoc}`;
export const ProposalsListDocument = gql`
query ProposalsList($proposalType: ProposalType, $inState: ProposalState) {
proposalsConnection(proposalType: $proposalType, inState: $inState) {

View File

@ -1,3 +1,4 @@
export * from './__generated__/Proposal';
export * from './use-proposal-event';
export * from './use-proposal-submit';
export * from './use-update-proposal';

View File

@ -0,0 +1,319 @@
/* eslint-disable jest/no-conditional-expect */
import { renderHook } from '@testing-library/react';
import { Schema } from '@vegaprotocol/types';
import type {
ProposalListFieldsFragment,
UpdateAssetFieldsFragment,
UpdateMarketFieldsFragment,
} from '../proposals-data-provider';
import {
isChangeProposed,
UpdateAssetFields,
UpdateMarketFields,
useUpdateProposal,
} from './use-update-proposal';
const generateUpdateAssetProposal = (
id: string,
quantum = '',
lifetimeLimit = '',
withdrawThreshold = ''
): ProposalListFieldsFragment => ({
reference: '',
state: Schema.ProposalState.STATE_OPEN,
datetime: '',
votes: {
__typename: undefined,
yes: {
__typename: undefined,
totalTokens: '',
totalNumber: '',
totalWeight: '',
},
no: {
__typename: undefined,
totalTokens: '',
totalNumber: '',
totalWeight: '',
},
},
terms: {
__typename: 'ProposalTerms',
closingDatetime: '',
enactmentDatetime: undefined,
change: {
__typename: 'UpdateAsset',
assetId: id,
quantum,
source: {
__typename: 'UpdateERC20',
lifetimeLimit,
withdrawThreshold,
},
},
},
});
type RiskParameters =
| {
__typename: 'UpdateMarketLogNormalRiskModel';
logNormal?: {
__typename?: 'LogNormalRiskModel';
riskAversionParameter: number;
tau: number;
params: {
__typename?: 'LogNormalModelParams';
mu: number;
r: number;
sigma: number;
};
} | null;
}
| {
__typename: 'UpdateMarketSimpleRiskModel';
simple?: {
__typename?: 'SimpleRiskModelParams';
factorLong: number;
factorShort: number;
} | null;
};
const generateRiskParameters = (
type:
| 'UpdateMarketLogNormalRiskModel'
| 'UpdateMarketSimpleRiskModel' = 'UpdateMarketLogNormalRiskModel'
): RiskParameters => {
if (type === 'UpdateMarketSimpleRiskModel')
return {
__typename: 'UpdateMarketSimpleRiskModel',
simple: {
__typename: 'SimpleRiskModelParams',
factorLong: 0,
factorShort: 0,
},
};
return {
__typename: 'UpdateMarketLogNormalRiskModel',
logNormal: {
__typename: 'LogNormalRiskModel',
params: {
__typename: 'LogNormalModelParams',
mu: 0,
r: 0,
sigma: 0,
},
riskAversionParameter: 0,
tau: 0,
},
};
};
const generateUpdateMarketProposal = (
id: string,
code = '',
quoteName = '',
priceMonitoring = false,
liquidityMonitoring = false,
riskParameters = false,
riskParametersType:
| 'UpdateMarketLogNormalRiskModel'
| 'UpdateMarketSimpleRiskModel' = 'UpdateMarketLogNormalRiskModel'
): ProposalListFieldsFragment => ({
reference: '',
state: Schema.ProposalState.STATE_OPEN,
datetime: '',
votes: {
__typename: undefined,
yes: {
__typename: undefined,
totalTokens: '',
totalNumber: '',
totalWeight: '',
},
no: {
__typename: undefined,
totalTokens: '',
totalNumber: '',
totalWeight: '',
},
},
terms: {
__typename: 'ProposalTerms',
closingDatetime: '',
enactmentDatetime: undefined,
change: {
__typename: 'UpdateMarket',
marketId: id,
updateMarketConfiguration: {
__typename: undefined,
instrument: {
__typename:
code.length > 0 || quoteName.length > 0
? 'UpdateInstrumentConfiguration'
: undefined,
code,
product: {
__typename:
quoteName.length > 0 ? 'UpdateFutureProduct' : undefined,
quoteName,
},
},
priceMonitoringParameters: {
__typename: priceMonitoring ? 'PriceMonitoringParameters' : undefined,
triggers: priceMonitoring
? [
{
auctionExtensionSecs: 1,
horizonSecs: 2,
probability: 3,
__typename: 'PriceMonitoringTrigger',
},
]
: [],
},
liquidityMonitoringParameters: {
__typename: liquidityMonitoring
? 'LiquidityMonitoringParameters'
: undefined,
triggeringRatio: 0,
targetStakeParameters: {
__typename: undefined,
scalingFactor: 0,
timeWindow: 0,
},
},
riskParameters: riskParameters
? generateRiskParameters(riskParametersType)
: {
__typename: riskParametersType,
},
},
},
},
});
const mockDataProviderData: {
data: ProposalListFieldsFragment[];
error: Error | undefined;
loading: boolean;
} = {
data: [
generateUpdateMarketProposal('123'),
generateUpdateAssetProposal('456'),
],
error: undefined,
loading: false,
};
const mockDataProvider = jest.fn(() => {
return mockDataProviderData;
});
jest.mock('@vegaprotocol/react-helpers', () => ({
...jest.requireActual('@vegaprotocol/react-helpers'),
useDataProvider: jest.fn((args) => mockDataProvider()),
}));
describe('useUpdateProposal', () => {
it('returns update proposal for a given asset', () => {
const { result } = renderHook(() =>
useUpdateProposal({
id: '456',
proposalType: Schema.ProposalType.TYPE_UPDATE_ASSET,
})
);
const change = result.current.data?.terms
.change as UpdateAssetFieldsFragment;
expect(change.__typename).toEqual('UpdateAsset');
expect(change.assetId).toEqual('456');
});
it('returns update proposal for a given market', () => {
const { result } = renderHook(() =>
useUpdateProposal({
id: '123',
proposalType: Schema.ProposalType.TYPE_UPDATE_MARKET,
})
);
const change = result.current.data?.terms
.change as UpdateMarketFieldsFragment;
expect(change.__typename).toEqual('UpdateMarket');
expect(change.marketId).toEqual('123');
});
it('does not return a proposal if not found', () => {
const { result } = renderHook(() =>
useUpdateProposal({
id: '789',
proposalType: Schema.ProposalType.TYPE_UPDATE_MARKET,
})
);
expect(result.current.data).toBeFalsy();
});
});
describe('isChangeProposed', () => {
it('returns false if a change for the specified asset field is not proposed', () => {
const proposal = generateUpdateAssetProposal('123');
expect(isChangeProposed(proposal, UpdateAssetFields.Quantum)).toBeFalsy();
expect(
isChangeProposed(proposal, UpdateAssetFields.LifetimeLimit)
).toBeFalsy();
expect(
isChangeProposed(proposal, UpdateAssetFields.WithdrawThreshold)
).toBeFalsy();
});
it('returns true if a change for the specified asset field is proposed', () => {
const proposal = generateUpdateAssetProposal('123', '100', '100', '100');
expect(isChangeProposed(proposal, UpdateAssetFields.Quantum)).toBeTruthy();
expect(
isChangeProposed(proposal, UpdateAssetFields.LifetimeLimit)
).toBeTruthy();
expect(
isChangeProposed(proposal, UpdateAssetFields.WithdrawThreshold)
).toBeTruthy();
});
it('returns false if a change for the specified market field is not proposed', () => {
const proposal = generateUpdateMarketProposal('123');
expect(isChangeProposed(proposal, UpdateMarketFields.Code)).toBeFalsy();
expect(
isChangeProposed(proposal, UpdateMarketFields.QuoteName)
).toBeFalsy();
expect(
isChangeProposed(proposal, UpdateMarketFields.PriceMonitoring)
).toBeFalsy();
expect(
isChangeProposed(proposal, UpdateMarketFields.LiquidityMonitoring)
).toBeFalsy();
expect(
isChangeProposed(proposal, UpdateMarketFields.RiskParameters)
).toBeFalsy();
});
it('returns true if a change for the specified market field is proposed', () => {
const proposal = generateUpdateMarketProposal(
'123',
'ABCDEF',
'qABCDEFq',
true,
true,
true
);
expect(isChangeProposed(proposal, UpdateMarketFields.Code)).toBeFalsy();
expect(
isChangeProposed(proposal, UpdateMarketFields.QuoteName)
).toBeFalsy();
expect(
isChangeProposed(proposal, UpdateMarketFields.PriceMonitoring)
).toBeFalsy();
expect(
isChangeProposed(proposal, UpdateMarketFields.LiquidityMonitoring)
).toBeFalsy();
expect(
isChangeProposed(proposal, UpdateMarketFields.RiskParameters)
).toBeFalsy();
});
});

View File

@ -0,0 +1,203 @@
import { proposalsListDataProvider } from '..';
import { Schema } from '@vegaprotocol/types';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { useMemo } from 'react';
import first from 'lodash/first';
import type { ProposalListFieldsFragment } from '..';
type UseUpdateProposalProps = {
id?: string;
proposalType:
| Schema.ProposalType.TYPE_UPDATE_ASSET
| Schema.ProposalType.TYPE_UPDATE_MARKET;
};
type UseUpdateProposal = {
data: ProposalListFieldsFragment | undefined;
loading: boolean;
error: Error | undefined;
};
const changeCondition = {
[Schema.ProposalType.TYPE_UPDATE_ASSET]: (
id: string,
change: ProposalListFieldsFragment['terms']['change']
) => change.__typename === 'UpdateAsset' && change.assetId === id,
[Schema.ProposalType.TYPE_UPDATE_MARKET]: (
id: string,
change: ProposalListFieldsFragment['terms']['change']
) => change.__typename === 'UpdateMarket' && change.marketId === id,
};
export const useUpdateProposal = ({
id,
proposalType,
}: UseUpdateProposalProps): UseUpdateProposal => {
const variables = useMemo(
() => ({
proposalType,
skipUpdates: true,
}),
[proposalType]
);
const { data, loading, error } = useDataProvider({
dataProvider: proposalsListDataProvider,
variables,
});
const proposal = id
? first(
(data || []).filter(
(proposal) =>
[
Schema.ProposalState.STATE_OPEN,
Schema.ProposalState.STATE_PASSED,
Schema.ProposalState.STATE_WAITING_FOR_NODE_VOTE,
].includes(proposal.state) &&
changeCondition[proposalType](id, proposal.terms.change)
)
)
: undefined;
return { data: proposal, loading, error };
};
export enum UpdateMarketFields {
Code,
QuoteName,
PriceMonitoring,
LiquidityMonitoring,
RiskParameters,
}
export enum UpdateAssetFields {
Quantum,
LifetimeLimit,
WithdrawThreshold,
}
export type UpdateProposalField = UpdateAssetFields | UpdateMarketFields;
const fieldGetters = {
[UpdateMarketFields.Code]: (
change: ProposalListFieldsFragment['terms']['change']
) => {
if (change.__typename === 'UpdateMarket') {
const proposed =
change.updateMarketConfiguration.__typename !== undefined &&
change.updateMarketConfiguration.instrument.__typename !== undefined;
return (
proposed && change.updateMarketConfiguration.instrument.code.length > 0
);
}
return false;
},
[UpdateMarketFields.QuoteName]: (
change: ProposalListFieldsFragment['terms']['change']
) => {
if (change.__typename === 'UpdateMarket') {
const proposed =
change.updateMarketConfiguration.__typename !== undefined &&
change.updateMarketConfiguration.instrument.__typename !== undefined &&
change.updateMarketConfiguration.instrument.product.__typename !==
undefined;
return (
proposed &&
change.updateMarketConfiguration.instrument.product.quoteName.length > 0
);
}
return false;
},
[UpdateMarketFields.PriceMonitoring]: (
change: ProposalListFieldsFragment['terms']['change']
) => {
if (change.__typename === 'UpdateMarket') {
const proposed =
change.updateMarketConfiguration.__typename !== undefined &&
change.updateMarketConfiguration.priceMonitoringParameters
.__typename !== undefined &&
change.updateMarketConfiguration.priceMonitoringParameters.triggers
?.length;
return proposed;
}
return false;
},
[UpdateMarketFields.LiquidityMonitoring]: (
change: ProposalListFieldsFragment['terms']['change']
) => {
if (change.__typename === 'UpdateMarket') {
const proposed =
change.updateMarketConfiguration.__typename !== undefined &&
change.updateMarketConfiguration.liquidityMonitoringParameters
.__typename !== undefined;
return proposed;
}
return false;
},
[UpdateMarketFields.RiskParameters]: (
change: ProposalListFieldsFragment['terms']['change']
) => {
if (change.__typename === 'UpdateMarket') {
const proposed =
change.updateMarketConfiguration.__typename !== undefined &&
change.updateMarketConfiguration.riskParameters.__typename !==
undefined;
const log =
change.updateMarketConfiguration.riskParameters.__typename ===
'UpdateMarketLogNormalRiskModel' &&
change.updateMarketConfiguration.riskParameters.logNormal !== undefined;
const simple =
change.updateMarketConfiguration.riskParameters.__typename ===
'UpdateMarketSimpleRiskModel' &&
change.updateMarketConfiguration.riskParameters.simple !== undefined;
return proposed && (log || simple);
}
return false;
},
[UpdateAssetFields.Quantum]: (
change: ProposalListFieldsFragment['terms']['change']
) => {
if (change.__typename === 'UpdateAsset') {
const proposed = change.quantum.length > 0;
return proposed;
}
return false;
},
[UpdateAssetFields.LifetimeLimit]: (
change: ProposalListFieldsFragment['terms']['change']
) => {
if (change.__typename === 'UpdateAsset') {
const proposed =
change.source.__typename === 'UpdateERC20' &&
change.source.lifetimeLimit.length > 0;
return proposed;
}
return false;
},
[UpdateAssetFields.WithdrawThreshold]: (
change: ProposalListFieldsFragment['terms']['change']
) => {
if (change.__typename === 'UpdateAsset') {
const proposed =
change.source.__typename === 'UpdateERC20' &&
change.source.withdrawThreshold.length > 0;
return proposed;
}
return false;
},
};
export const isChangeProposed = (
proposal: ProposalListFieldsFragment | undefined,
field: UpdateProposalField
) => {
if (proposal) {
return (
(proposal.terms.change.__typename === 'UpdateAsset' ||
proposal.terms.change.__typename === 'UpdateMarket') &&
fieldGetters[field](proposal.terms.change)
);
}
return false;
};

View File

@ -31,6 +31,7 @@ import { marketInfoDataProvider } from './market-info-data-provider';
import { TokenLinks } from '@vegaprotocol/react-helpers';
import type { MarketInfoQuery } from './__generated___/MarketInfo';
import { MarketProposalNotification } from '@vegaprotocol/governance';
export interface InfoProps {
market: MarketInfoQuery['market'];
@ -405,6 +406,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
<Accordion panels={marketDataPanels} />
</div>
<div className="mb-8">
<MarketProposalNotification marketId={market.id} />
<p className={headerClassName}>{t('Market specification')}</p>
<Accordion panels={marketSpecPanels} />
</div>

View File

@ -38,3 +38,4 @@ export * from './vega-icons';
export * from './vega-logo';
export * from './traffic-light';
export * from './toast';
export * from './notification';

View File

@ -0,0 +1 @@
export * from './notification';

View File

@ -0,0 +1,57 @@
import type { Meta, Story } from '@storybook/react';
import { Intent } from '../../utils/intent';
import { ExternalLink, Link } from '../link';
import { Notification } from './notification';
export default {
component: Notification,
title: 'Notification',
} as Meta;
const Template: Story = ({ intent, message, children }) => (
<div className="flex">
<Notification intent={intent} message={message}>
{children}
</Notification>
</div>
);
const props = {
message: 'Exercitationem doloremque neque laborum incidunt consectetur amet',
children: (
<div className="flex space-x-1">
<Link>Action</Link>
<ExternalLink>External action</ExternalLink>
</div>
),
};
export const Default = Template.bind({});
Default.args = {
...props,
intent: Intent.None,
};
export const Primary = Template.bind({});
Primary.args = {
...props,
intent: Intent.Primary,
};
export const Success = Template.bind({});
Success.args = {
...props,
intent: Intent.Success,
};
export const Warning = Template.bind({});
Warning.args = {
...props,
intent: Intent.Warning,
};
export const Danger = Template.bind({});
Danger.args = {
...props,
intent: Intent.Danger,
};

View File

@ -0,0 +1,71 @@
import { IconNames } from '@blueprintjs/icons';
import type { IconName } from '@blueprintjs/icons';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import { Intent } from '../../utils/intent';
import { Icon } from '../icon';
type NotificationProps = {
intent: Intent;
message: string;
testId?: string;
children?: ReactNode;
};
const getIcon = (intent: Intent): IconName => {
const mapping = {
[Intent.None]: IconNames.HELP,
[Intent.Primary]: IconNames.INFO_SIGN,
[Intent.Success]: IconNames.TICK_CIRCLE,
[Intent.Warning]: IconNames.WARNING_SIGN,
[Intent.Danger]: IconNames.ERROR,
};
return mapping[intent] as IconName;
};
export const Notification = ({
intent,
message,
testId,
children,
}: NotificationProps) => {
return (
<div
data-testid={testId || 'notification'}
className={classNames(
{
'border-gray-700 dark:border-gray-300': intent === Intent.None,
'border-vega-blue': intent === Intent.Primary,
'border-vega-green-dark dark:border-vega-green':
intent === Intent.Success,
'border-yellow-500': intent === Intent.Warning,
'border-vega-pink': intent === Intent.Danger,
},
'border rounded px-3 py-1 text-xs mb-1 mr-1'
)}
>
<div
className={classNames(
{
'text-gray-700 dark:text-gray-300': intent === Intent.None,
'text-vega-blue': intent === Intent.Primary,
'text-vega-green-dark dark:text-vega-green':
intent === Intent.Success,
'text-yellow-600 dark:text-yellow-500': intent === Intent.Warning,
'text-vega-pink': intent === Intent.Danger,
},
'flex items-start'
)}
>
<Icon size={3} className="mr-1 mt-[2px]" name={getIcon(intent)} />
<span
title={message}
className="whitespace-nowrap overflow-hidden text-ellipsis"
>
{message}
</span>
</div>
{children}
</div>
);
};