Compare commits

..
22 changed files with 772 additions and 238 deletions
-4
View File
@@ -34,10 +34,6 @@ export const TOP_LEVEL_ROUTES = [
name: 'Rewards',
path: Routes.REWARDS,
},
{
name: 'Restricted',
path: Routes.RESTRICTED,
},
];
export const TOKEN_DROPDOWN_ROUTES = [
@@ -0,0 +1,23 @@
import {
AssetDetailsDialog,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { useUpdateProposal } from '@vegaprotocol/proposals';
import * as Schema from '@vegaprotocol/types';
export const LocalAssetDetailsDialog = () => {
const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore();
const { data: proposal } = useUpdateProposal({
id,
proposalType: Schema.ProposalType.TYPE_UPDATE_ASSET,
});
return (
<AssetDetailsDialog
assetId={id}
trigger={trigger || null}
open={isOpen}
onChange={setOpen}
proposalId={proposal?.id ?? undefined}
/>
);
};
@@ -0,0 +1 @@
export * from './asset-details-dialog';
@@ -1,6 +1,9 @@
import { Fragment, useState } from 'react';
import type { NewMarketSuccessorFieldsFragment } from '@vegaprotocol/proposals';
import { useMarketViewProposals } from '@vegaprotocol/proposals';
import {
marketViewProposalsDataProvider,
type NewMarketSuccessorFieldsFragment,
} from '@vegaprotocol/proposals';
import {
ExternalLink,
Intent,
@@ -9,17 +12,20 @@ import {
import { t } from '@vegaprotocol/i18n';
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import * as Types from '@vegaprotocol/types';
import { useDataProvider } from '@vegaprotocol/data-provider';
export const MarketSuccessorProposalBanner = ({
marketId,
}: {
marketId?: string;
}) => {
const proposals = useMarketViewProposals({
const { data: proposals } = useDataProvider({
dataProvider: marketViewProposalsDataProvider,
skip: !marketId,
inState: Types.ProposalState.STATE_OPEN,
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
typename: 'NewMarket',
variables: {
inState: Types.ProposalState.STATE_OPEN,
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
},
});
const successors =
@@ -20,10 +20,13 @@ const marketMock = {
},
} as Market;
const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
const passedProposalMock: MockedResponse<MarketViewProposalsQuery> = {
request: {
query: MarketViewProposalsDocument,
variables: { inState: Types.ProposalState.STATE_PASSED },
variables: {
inState: Types.ProposalState.STATE_PASSED,
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
},
},
result: {
data: {
@@ -31,7 +34,7 @@ const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
edges: [
{
node: {
id: 'first-id',
id: '1',
state: Types.ProposalState.STATE_PASSED,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
@@ -56,7 +59,7 @@ const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
},
{
node: {
id: 'second-id',
id: '2',
state: Types.ProposalState.STATE_PASSED,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
@@ -84,7 +87,99 @@ const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
},
},
};
const mocks: MockedResponse[] = [proposalMock];
const openProposalMock: MockedResponse<MarketViewProposalsQuery> = {
request: {
query: MarketViewProposalsDocument,
variables: {
inState: Types.ProposalState.STATE_OPEN,
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
},
},
result: {
data: {
proposalsConnection: {
edges: [
{
node: {
id: '3',
state: Types.ProposalState.STATE_OPEN,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
enactmentDatetime: '2023-10-01T11:48:18',
change: {
__typename: 'UpdateMarketState',
updateType:
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
price: '',
market: {
id: 'market-3',
tradableInstrument: {
instrument: {
name: 'Market three name',
code: 'Market three',
},
},
},
},
},
},
},
{
node: {
id: '4',
state: Types.ProposalState.STATE_OPEN,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
enactmentDatetime: '2023-10-11T11:48:18',
change: {
__typename: 'UpdateMarketState',
updateType:
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
price: '',
market: {
id: 'market-3',
tradableInstrument: {
instrument: {
name: 'Market three name',
code: 'Market three',
},
},
},
},
},
},
},
{
node: {
id: '5',
state: Types.ProposalState.STATE_OPEN,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
enactmentDatetime: '2023-10-01T11:48:18',
change: {
__typename: 'UpdateMarketState',
updateType:
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
price: '',
market: {
id: 'market-4',
tradableInstrument: {
instrument: {
name: 'Market four name',
code: 'Market four',
},
},
},
},
},
},
},
],
},
},
},
};
const mocks: MockedResponse[] = [passedProposalMock, openProposalMock];
describe('MarketTerminationBanner', () => {
beforeAll(() => {
@@ -108,4 +203,28 @@ describe('MarketTerminationBanner', () => {
screen.getByTestId('termination-warning-banner-market-1')
).toBeInTheDocument();
});
it('should render link to proposals', async () => {
const { container } = render(
<MockedProvider mocks={mocks}>
<MarketTerminationBanner market={{ ...marketMock, id: 'market-3' }} />
</MockedProvider>
);
await waitFor(() => {
expect(container).not.toBeEmptyDOMElement();
});
expect(screen.getByText('View proposals')).toBeInTheDocument();
});
it('should render link to proposal', async () => {
const { container } = render(
<MockedProvider mocks={mocks}>
<MarketTerminationBanner market={{ ...marketMock, id: 'market-4' }} />
</MockedProvider>
);
await waitFor(() => {
expect(container).not.toBeEmptyDOMElement();
});
expect(screen.getByText('View proposal')).toBeInTheDocument();
});
});
@@ -1,12 +1,67 @@
import type { ReactNode } from 'react';
import { useState } from 'react';
import { format, formatDuration, intervalToDuration } from 'date-fns';
import { Intent, NotificationBanner } from '@vegaprotocol/ui-toolkit';
import { useMarketViewProposals } from '@vegaprotocol/proposals';
import {
ExternalLink,
Intent,
NotificationBanner,
} from '@vegaprotocol/ui-toolkit';
import type { MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
import { marketViewProposalsDataProvider } from '@vegaprotocol/proposals';
import { t } from '@vegaprotocol/i18n';
import * as Types from '@vegaprotocol/types';
import type { Market } from '@vegaprotocol/markets';
import { getQuoteName } from '@vegaprotocol/markets';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import sortBy from 'lodash/sortBy';
import {
DApp,
TOKEN_PROPOSAL,
TOKEN_PROPOSALS,
useLinks,
} from '@vegaprotocol/environment';
import { useDataProvider } from '@vegaprotocol/data-provider';
const filterProposals = (
data: MarketViewProposalFieldsFragment[] | null,
marketId: string,
now: number
) =>
sortBy(
(data || []).filter(
(item) =>
item.terms.change.__typename === 'UpdateMarketState' &&
item.terms.change.market.id === marketId &&
item.terms.change.updateType ===
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE &&
item.terms.enactmentDatetime &&
new Date(item.terms.enactmentDatetime).getTime() > now
),
(item) => item.terms.enactmentDatetime
);
const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => {
const enactmentDatetime = new Date(proposal.terms.enactmentDatetime);
const date = format(enactmentDatetime, 'dd MMMM');
const duration = formatDuration(
intervalToDuration({
start: new Date(),
end: enactmentDatetime,
}),
{
format: ['days', 'hours'],
}
);
const price =
proposal.terms.change.__typename === 'UpdateMarketState'
? proposal.terms.change.price
: '';
return {
date,
duration,
price,
};
};
export const MarketTerminationBanner = ({
market,
@@ -15,57 +70,55 @@ export const MarketTerminationBanner = ({
}) => {
const [visible, setVisible] = useState(true);
const skip = !market || !visible;
const proposalsData = useMarketViewProposals({
const { data: passedProposalsData } = useDataProvider({
dataProvider: marketViewProposalsDataProvider,
skip,
inState: Types.ProposalState.STATE_PASSED,
typename: 'UpdateMarketState',
variables: {
inState: Types.ProposalState.STATE_PASSED,
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
},
});
const { data: openProposalsData } = useDataProvider({
dataProvider: marketViewProposalsDataProvider,
skip,
variables: {
inState: Types.ProposalState.STATE_OPEN,
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
},
});
const governanceLink = useLinks(DApp.Governance);
if (!market) return null;
const marketFound = (proposalsData || []).find(
(item) =>
item.terms.change.__typename === 'UpdateMarketState' &&
item.terms.change.market.id === market.id &&
item.terms.change.updateType ===
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE &&
item.state === Types.ProposalState.STATE_PASSED // subscription doesn't have state parameter
);
const now = Date.now();
const passedProposals = filterProposals(passedProposalsData, market.id, now);
const openProposals = filterProposals(openProposalsData, market.id, now);
const enactmentDatetime = new Date(marketFound?.terms.enactmentDatetime);
const name =
marketFound?.terms.change.__typename === 'UpdateMarketState'
? marketFound.terms.change.market.tradableInstrument.instrument.code
: '';
const name = market.tradableInstrument.instrument.code;
if (!passedProposals.length && !openProposals.length) {
return null;
}
if (name && enactmentDatetime.getTime() > Date.now()) {
const dayMonthDate = format(enactmentDatetime, 'dd MMMM');
const duration = intervalToDuration({
start: new Date(),
end: enactmentDatetime,
});
const formattedDuration = formatDuration(duration, {
format: ['days', 'hours'],
});
const price =
marketFound?.terms.change.__typename === 'UpdateMarketState'
? marketFound.terms.change.price
: '';
const assetSymbol = getQuoteName(market);
return (
<NotificationBanner
intent={Intent.Warning}
onClose={() => {
setVisible(false);
}}
data-testid={`termination-warning-banner-${market.id}`}
>
const assetSymbol = getQuoteName(market);
const proposalLink =
!passedProposals.length && openProposals[0]?.id
? governanceLink(TOKEN_PROPOSAL.replace(':id', openProposals[0]?.id))
: undefined;
const proposalsLink =
openProposals.length > 1 ? governanceLink(TOKEN_PROPOSALS) : undefined;
let content: ReactNode;
if (passedProposals.length) {
const { date, duration, price } = getMessageVariables(passedProposals[0]);
content = (
<>
<div className="uppercase mb-1">
{t('Trading on Market %s will stop on %s', [name, dayMonthDate])}
{t('Trading on Market %s will stop on %s', [name, date])}
</div>
<div>
{t(
'You will no longer be able to hold a position on this market when it closes in %s.',
[formattedDuration]
[duration]
)}{' '}
{price &&
assetSymbol &&
@@ -74,8 +127,57 @@ export const MarketTerminationBanner = ({
assetSymbol,
])}
</div>
</NotificationBanner>
</>
);
} else if (openProposals.length > 1) {
content = (
<>
<div className="uppercase mb-1">
{t(
'Trading on Market %s may stop. There are open proposals to close this market',
[name]
)}
</div>
<div>
<ExternalLink href={proposalsLink}>
{t('View proposals')}
</ExternalLink>
</div>
</>
);
} else {
const { date, price } = getMessageVariables(openProposals[0]);
content = (
<>
<div className="uppercase mb-1">
{t(
'Trading on Market %s may stop on %s. There is open proposal to close this market.',
[name, date]
)}
</div>
<div>
{price &&
assetSymbol &&
t('Proposed final price is %s %s.', [
addDecimalsFormatNumber(price, market.decimalPlaces),
assetSymbol,
])}
</div>
<div>
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
</div>
</>
);
}
return null;
return (
<NotificationBanner
intent={openProposals.length ? Intent.Warning : Intent.Info}
onClose={() => {
setVisible(false);
}}
data-testid={`termination-warning-banner-${market.id}`}
>
{content}
</NotificationBanner>
);
};
+2 -11
View File
@@ -1,7 +1,3 @@
import {
AssetDetailsDialog,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { VegaConnectDialog, ViewAsDialog } from '@vegaprotocol/wallet';
import { Connectors } from '../lib/vega-connectors';
import {
@@ -10,9 +6,9 @@ import {
} from '@vegaprotocol/web3';
import { WelcomeDialog } from '../components/welcome-dialog';
import { RiskMessage } from '../components/welcome-dialog';
import { LocalAssetDetailsDialog as AssetDetailsDialog } from '../components/asset-details-dialog';
const DialogsContainer = () => {
const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore();
return (
<>
<VegaConnectDialog
@@ -20,12 +16,7 @@ const DialogsContainer = () => {
riskMessage={<RiskMessage />}
/>
<ViewAsDialog connector={Connectors.view} />
<AssetDetailsDialog
assetId={id}
trigger={trigger || null}
open={isOpen}
onChange={setOpen}
/>
<AssetDetailsDialog />
<WelcomeDialog />
<Web3ConnectUncontrolledDialog />
<WithdrawalApprovalDialogContainer />
+2 -2
View File
@@ -1,5 +1,5 @@
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
import { useUpdateNetworkParametersToasts } from '@vegaprotocol/proposals';
import { useProposalNotificationToasts } from '@vegaprotocol/proposals';
import { useVegaTransactionToasts } from '@vegaprotocol/web3';
import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
@@ -7,7 +7,7 @@ import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
import { Links } from '../lib/links';
export const ToastsManager = () => {
useUpdateNetworkParametersToasts();
useProposalNotificationToasts();
useVegaTransactionToasts();
useEthereumTransactionToasts();
useEthereumWithdrawApprovalsToasts();
+4 -2
View File
@@ -9,7 +9,7 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { create } from 'zustand';
import { AssetDetailsTable } from './asset-details-table';
import { AssetProposalNotification } from '@vegaprotocol/proposals';
import { AssetProposalNotification } from './asset-proposal-notification';
import { useAssetDataProvider } from './asset-data-provider';
export type AssetDetailsDialogStore = {
@@ -47,6 +47,7 @@ export interface AssetDetailsDialogProps {
open: boolean;
onChange: (open: boolean) => void;
asJson?: boolean;
proposalId?: string;
}
export const AssetDetailsDialog = ({
@@ -55,6 +56,7 @@ export const AssetDetailsDialog = ({
open,
onChange,
asJson = false,
proposalId,
}: AssetDetailsDialogProps) => {
const { data: asset } = useAssetDataProvider(assetId);
@@ -62,7 +64,7 @@ export const AssetDetailsDialog = ({
const content = asset ? (
<div className="my-2">
<AssetProposalNotification assetId={asset.id} />
<AssetProposalNotification proposalId={proposalId} />
{asJson ? (
<div className="pr-8">
<SyntaxHighlighter size="smaller" data={asset} />
@@ -1,24 +1,18 @@
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
import { useUpdateProposal } from '../lib';
type AssetProposalNotificationProps = {
assetId?: string;
proposalId?: string;
};
export const AssetProposalNotification = ({
assetId,
proposalId,
}: AssetProposalNotificationProps) => {
const tokenLink = useLinks(DApp.Governance);
const { data: proposal } = useUpdateProposal({
id: assetId,
proposalType: Schema.ProposalType.TYPE_UPDATE_ASSET,
});
if (proposal) {
if (proposalId) {
const proposalLink = tokenLink(
TOKEN_PROPOSAL.replace(':id', proposal.id || '')
TOKEN_PROPOSAL.replace(':id', proposalId || '')
);
const message = (
<>
-1
View File
@@ -1,4 +1,3 @@
export * from './asset-proposal-notification';
export * from './market-proposal-notification';
export * from './proposals-list';
export * from './protocol-upgrade-countdown';
@@ -319,6 +319,7 @@ fragment NewAssetFields on NewAsset {
decimals
quantum
source {
__typename
... on BuiltinAsset {
maxFaucetAmountMint
}
@@ -488,3 +489,39 @@ subscription MarketViewLiveProposals {
...MarketViewProposalFields
}
}
fragment NotificationProposalsFields on Proposal {
id
state
terms {
closingDatetime
enactmentDatetime
change {
__typename
... on UpdateMarketState {
...UpdateMarketStateFields
}
... on NewMarket {
...NewMarketSuccessorFields
}
... on UpdateMarket {
...UpdateMarketFields
}
... on NewAsset {
...NewAssetFields
}
... on UpdateAsset {
...UpdateAssetFields
}
... on UpdateNetworkParameter {
...UpdateNetworkParameterFields
}
}
}
}
subscription NotificationProposals {
proposals {
...NotificationProposalsFields
}
}
File diff suppressed because one or more lines are too long
@@ -1,2 +1,5 @@
export * from './proposals-data-provider';
export {
marketViewProposalsDataProvider,
proposalsDataProvider,
} from './proposals-data-provider';
export * from './__generated__/Proposals';
@@ -0,0 +1,67 @@
import { update } from './proposals-data-provider';
import type {
MarketViewProposalFieldsFragment,
MarketViewProposalsQueryVariables,
} from './__generated__/Proposals';
import * as Types from '@vegaprotocol/types';
describe('proposals data provider', () => {
const proposal = {
id: '1',
state: Types.ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarketState',
},
},
} as MarketViewProposalFieldsFragment;
const data = [proposal];
const delta = { ...proposal };
const reload = jest.fn();
const variables: MarketViewProposalsQueryVariables = {
inState: Types.ProposalState.STATE_OPEN,
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
};
it('update existing data', () => {
expect(update(data, delta, reload, variables)?.[0]).toBe(delta);
});
it('removes existing data if delta do not match inState', () => {
expect(
update(
data,
{ ...delta, state: Types.ProposalState.STATE_PASSED },
reload,
variables
)?.length
).toBe(0);
});
it('do not add delta if it do not match inState', () => {
expect(
update(
data,
{ ...delta, id: '2', state: Types.ProposalState.STATE_PASSED },
reload,
variables
)?.length
).toBe(1);
});
it('do not add delta if it do not match proposalType', () => {
expect(
update(
data,
{
...delta,
id: '2',
terms: { ...delta.terms, change: { __typename: 'UpdateMarket' } },
},
reload,
variables
)?.length
).toBe(1);
});
});
@@ -1,5 +1,7 @@
import type { Update } from '@vegaprotocol/data-provider';
import { makeDataProvider } from '@vegaprotocol/data-provider';
import produce from 'immer';
import * as Types from '@vegaprotocol/types';
import type {
ProposalsListQuery,
ProposalsListQueryVariables,
@@ -8,6 +10,7 @@ import type {
MarketViewProposalFieldsFragment,
MarketViewProposalsQuery,
MarketViewProposalsQueryVariables,
MarketViewLiveProposalsSubscriptionVariables,
} from './__generated__/Proposals';
import {
MarketViewLiveProposalsDocument,
@@ -42,21 +45,61 @@ export const proposalsDataProvider = makeDataProvider<
errors.every((e) => e.message.match(/failed to get asset for ID/)),
});
const update = (
const ProposalTypeMap: Record<
Types.ProposalType,
Types.ProposalChange['__typename']
> = {
[Types.ProposalType.TYPE_CANCEL_TRANSFER]: 'CancelTransfer',
[Types.ProposalType.TYPE_NETWORK_PARAMETERS]: 'UpdateNetworkParameter',
[Types.ProposalType.TYPE_NEW_ASSET]: 'NewAsset',
[Types.ProposalType.TYPE_NEW_FREE_FORM]: 'NewFreeform',
[Types.ProposalType.TYPE_NEW_MARKET]: 'NewMarket',
[Types.ProposalType.TYPE_NEW_SPOT_MARKET]: 'NewSpotMarket',
[Types.ProposalType.TYPE_NEW_TRANSFER]: 'NewTransfer',
[Types.ProposalType.TYPE_UPDATE_ASSET]: 'UpdateAsset',
[Types.ProposalType.TYPE_UPDATE_MARKET]: 'UpdateMarket',
[Types.ProposalType.TYPE_UPDATE_MARKET_STATE]: 'UpdateMarketState',
[Types.ProposalType.TYPE_UPDATE_REFERRAL_PROGRAM]: 'UpdateReferralProgram',
[Types.ProposalType.TYPE_UPDATE_SPOT_MARKET]: 'UpdateSpotMarket',
[Types.ProposalType.TYPE_UPDATE_VOLUME_DISCOUNT_PROGRAM]:
'UpdateVolumeDiscountProgram',
};
const matchFilter = (
data: MarketViewProposalFieldsFragment,
variables: MarketViewProposalsQueryVariables
) => {
return (
(!variables.inState || data.state === variables.inState) &&
(!variables.proposalType ||
data.terms.change.__typename === ProposalTypeMap[variables.proposalType])
);
};
export const update: Update<
MarketViewProposalFieldsFragment[] | null,
MarketViewProposalFieldsFragment,
MarketViewProposalsQueryVariables
> = (
data: MarketViewProposalFieldsFragment[] | null,
delta: MarketViewProposalFieldsFragment
delta: MarketViewProposalFieldsFragment,
reload,
variables
) => {
const updateData = produce(data || [], (draft) => {
const { id } = delta;
const index = draft.findIndex((item) => item.id === id);
const match = matchFilter(delta, variables);
if (index === -1) {
draft.unshift(delta);
if (match) {
draft.unshift(delta);
}
} else {
const currNode = draft[index];
draft[index] = {
...currNode,
...delta,
};
if (match) {
draft[index] = delta;
} else {
draft.splice(index, 1);
}
}
});
return updateData;
@@ -66,12 +109,15 @@ const getMarketProposalsData = (
responseData: MarketViewProposalsQuery | null
) => removePaginationWrapper(responseData?.proposalsConnection?.edges) || [];
const subscriptionVariables: MarketViewLiveProposalsSubscriptionVariables = {};
export const marketViewProposalsDataProvider = makeDataProvider<
MarketViewProposalsQuery,
MarketViewProposalFieldsFragment[],
MarketViewLiveProposalsSubscription,
MarketViewProposalFieldsFragment,
MarketViewProposalsQueryVariables
MarketViewProposalsQueryVariables,
MarketViewLiveProposalsSubscriptionVariables
>({
query: MarketViewProposalsDocument,
subscriptionQuery: MarketViewLiveProposalsDocument,
@@ -79,4 +125,5 @@ export const marketViewProposalsDataProvider = makeDataProvider<
getDelta: (subscriptionData: MarketViewLiveProposalsSubscription) =>
subscriptionData.proposals,
getData: getMarketProposalsData,
getSubscriptionVariables: () => subscriptionVariables,
});
@@ -3,8 +3,7 @@ export * from './use-proposal-event';
export * from './use-vega-transaction';
export * from './use-proposal-submit';
export * from './use-update-proposal';
export * from './use-update-network-paramaters-toasts';
export * from './use-proposal-notification-toasts';
export * from './use-successor-market-proposal-details';
export * from './use-new-transfer-proposal-details';
export * from './use-cancel-transfer-proposal-details';
export * from './use-market-view-proposals';
@@ -1,29 +0,0 @@
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketViewProposalsDataProvider } from '../proposals-data-provider';
import type * as Types from '@vegaprotocol/types';
export const useMarketViewProposals = ({
skip = false,
typename,
proposalType,
inState,
}: {
typename: Types.ProposalChange['__typename'];
skip?: boolean;
inState: Types.ProposalState;
proposalType?: Types.ProposalType;
}) => {
const variables = {
inState,
...(proposalType ? { proposalType } : null),
};
const { data } = useDataProvider({
dataProvider: marketViewProposalsDataProvider,
skip,
variables,
});
return (data || []).filter(
(item) => item.terms.change.__typename === typename
);
};
@@ -5,21 +5,21 @@ import { ProposalState } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import {
PROPOSAL_STATES_TO_TOAST,
useUpdateNetworkParametersToasts,
} from './use-update-network-paramaters-toasts';
useProposalNotificationToasts,
} from './use-proposal-notification-toasts';
import type {
UpdateNetworkParameterProposalFragment,
OnUpdateNetworkParametersSubscription,
} from './__generated__/Proposal';
import { OnUpdateNetworkParametersDocument } from './__generated__/Proposal';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { waitFor, renderHook } from '@testing-library/react';
import { NotificationProposalsDocument } from '../proposals-data-provider/__generated__/Proposals';
const render = (mocks?: MockedResponse[]) => {
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks}>{children}</MockedProvider>
);
return renderHook(() => useUpdateNetworkParametersToasts(), { wrapper });
return renderHook(() => useProposalNotificationToasts(), { wrapper });
};
const generateUpdateNetworkParametersProposal = (
@@ -51,7 +51,7 @@ const clear = () => {
useToasts.setState(INITIAL);
};
describe('useUpdateNetworkParametersToasts', () => {
describe('useProposalNotificationToasts', () => {
beforeEach(clear);
afterAll(clear);
@@ -61,7 +61,7 @@ describe('useUpdateNetworkParametersToasts', () => {
const mockOpenProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
{
request: {
query: OnUpdateNetworkParametersDocument,
query: NotificationProposalsDocument,
},
result: {
data: {
@@ -89,7 +89,7 @@ describe('useUpdateNetworkParametersToasts', () => {
const mockFailedProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
{
request: {
query: OnUpdateNetworkParametersDocument,
query: NotificationProposalsDocument,
},
result: {
data: {
@@ -117,7 +117,7 @@ describe('useUpdateNetworkParametersToasts', () => {
const mockEmptyProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
{
request: {
query: OnUpdateNetworkParametersDocument,
query: NotificationProposalsDocument,
},
result: {
data: {
@@ -142,7 +142,7 @@ describe('useUpdateNetworkParametersToasts', () => {
{
terms: {
change: {
__typename: 'NewMarket',
__typename: 'NewSpotMarket',
},
},
}
@@ -150,7 +150,7 @@ describe('useUpdateNetworkParametersToasts', () => {
const mockWrongProposalType: MockedResponse<OnUpdateNetworkParametersSubscription> =
{
request: {
query: OnUpdateNetworkParametersDocument,
query: NotificationProposalsDocument,
},
result: {
data: {
@@ -0,0 +1,200 @@
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { ProposalStateMapping, MarketUpdateType } from '@vegaprotocol/types';
import { ProposalState } from '@vegaprotocol/types';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import { useCallback } from 'react';
import type { NotificationProposalsFieldsFragment } from '../proposals-data-provider';
import { useNotificationProposalsSubscription } from '../proposals-data-provider';
export const PROPOSAL_STATES_TO_TOAST = [
ProposalState.STATE_DECLINED,
ProposalState.STATE_ENACTED,
ProposalState.STATE_OPEN,
ProposalState.STATE_PASSED,
];
const PROPOSAL_TYPES_TO_TOAST = [
'UpdateNetworkParameter',
'NewAsset',
'NewMarket',
'UpdateAsset',
'UpdateMarket',
'UpdateMarketState',
];
type Proposal = NotificationProposalsFieldsFragment;
const NotificationProposalToastContent = ({
proposal,
}: {
proposal: Proposal;
}) => {
const { data: assets } = useAssetsDataProvider();
const tokenLink = useLinks(DApp.Governance);
const change = proposal.terms.change;
let pretitle = 'New';
let content: ReactNode = null;
switch (change.__typename) {
case 'UpdateNetworkParameter':
pretitle = t('Network change');
content = (
<p className="italic">
'{t('Update ')}
<span className="break-all">{change.networkParameter.key}</span>
{t(' to ')}
<span>{change.networkParameter.value}</span>'
</p>
);
break;
case 'NewMarket':
pretitle = t('New market');
content = (
<p>
{t('New market ')}
<span className="break-all">{change.instrument.name}</span>
{change.successorConfiguration?.parentMarketId && (
<span>
{' '}
{t('successor of %', [
change.successorConfiguration.parentMarketId,
])}
</span>
)}
</p>
);
break;
case 'UpdateMarket':
pretitle = t('Update market');
content = (
<p>
{t('Market ')}
<span className="break-all">
{change.updateMarketConfiguration.instrument.code}
</span>
</p>
);
break;
case 'UpdateMarketState':
if (
change.updateType ===
MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE
) {
pretitle = t('Market termination');
} else if (
change.updateType === MarketUpdateType.MARKET_STATE_UPDATE_TYPE_SUSPEND
) {
pretitle = t('Market suspension');
} else {
pretitle = t('Market resume');
}
content = (
<p>
{t('Market ')}
<span className="break-all">
{change.market.tradableInstrument.instrument.name}
</span>
{change.price && (
<span> {t('Final price will be %s', [change.price])}</span>
)}
</p>
);
break;
case 'NewAsset':
pretitle = t('New asset');
content = (
<p>
<span className="break-all">
{change.name} ({change.symbol})
</span>
{', '}
{t('type %s', [
change.source.__typename === 'ERC20' ? 'ERC20' : 'Builtin asset',
])}
</p>
);
break;
case 'UpdateAsset':
pretitle = t('Update asset');
content = (
<p>
<span className="break-all">
{assets?.find((asset) => asset.id === change.assetId)?.name ??
change.assetId}
</span>
</p>
);
break;
default:
}
const title = t('%s proposal %s', [
pretitle,
ProposalStateMapping[proposal.state].toLowerCase(),
]);
const enactment = Date.parse(proposal.terms.enactmentDatetime);
return (
<div>
<ToastHeading>{title}</ToastHeading>
{content}
{!isNaN(enactment) && (
<p>
{t('Enactment date:')} {getDateTimeFormat().format(enactment)}
</p>
)}
<p>
<ExternalLink
href={tokenLink(TOKEN_PROPOSAL).replace(':id', proposal?.id || '')}
>
{t('View proposal details')}
</ExternalLink>
</p>
</div>
);
};
export const useProposalNotificationToasts = () => {
const setToast = useToasts((store) => store.setToast);
const remove = useToasts((store) => store.remove);
const hasToast = useToasts((store) => store.hasToast);
const fromProposal = useCallback(
(proposal: Proposal): Toast => {
const id = `proposal-notification-${proposal.id}`;
return {
id,
intent: Intent.Warning,
content: <NotificationProposalToastContent proposal={proposal} />,
onClose: () => {
remove(id);
},
};
},
[remove]
);
const displayToast = (toast: Toast) => {
if (!hasToast(toast.id)) {
setToast(toast);
}
};
return useNotificationProposalsSubscription({
onData: ({ data }) => {
// note proposals is poorly named, it is actually a single proposal
const proposal = data.data?.proposals;
if (!proposal) return;
if (!PROPOSAL_TYPES_TO_TOAST.includes(proposal.terms.change.__typename)) {
return;
}
// if one of the following states show a toast
if (PROPOSAL_STATES_TO_TOAST.includes(proposal.state)) {
displayToast(fromProposal(proposal));
}
},
});
};
@@ -1,96 +0,0 @@
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { UpdateNetworkParameter } from '@vegaprotocol/types';
import { ProposalStateMapping } from '@vegaprotocol/types';
import { ProposalState } from '@vegaprotocol/types';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
import { useCallback } from 'react';
import type { UpdateNetworkParameterProposalFragment } from './__generated__/Proposal';
import { useOnUpdateNetworkParametersSubscription } from './__generated__/Proposal';
export const PROPOSAL_STATES_TO_TOAST = [
ProposalState.STATE_DECLINED,
ProposalState.STATE_ENACTED,
ProposalState.STATE_OPEN,
ProposalState.STATE_PASSED,
];
const CLOSE_AFTER = 0;
type Proposal = UpdateNetworkParameterProposalFragment;
const UpdateNetworkParameterToastContent = ({
proposal,
}: {
proposal: Proposal;
}) => {
const tokenLink = useLinks(DApp.Governance);
const change = proposal.terms.change as UpdateNetworkParameter;
const title = t('Network change proposal %s').replace(
'%s',
ProposalStateMapping[proposal.state].toLowerCase()
);
const enactment = Date.parse(proposal.terms.enactmentDatetime);
return (
<div>
<ToastHeading>{title}</ToastHeading>
<p className="italic">
'{t('Update ')}
<span className="break-all">{change.networkParameter.key}</span>
{t(' to ')}
<span>{change.networkParameter.value}</span>'
</p>
{!isNaN(enactment) && (
<p>
{t('Enactment date:')} {getDateTimeFormat().format(enactment)}
</p>
)}
<p>
<ExternalLink
href={tokenLink(TOKEN_PROPOSAL).replace(':id', proposal?.id || '')}
>
{t('View proposal details')}
</ExternalLink>
</p>
</div>
);
};
export const useUpdateNetworkParametersToasts = () => {
const { setToast, remove } = useToasts((store) => ({
setToast: store.setToast,
remove: store.remove,
}));
const fromProposal = useCallback(
(proposal: Proposal): Toast => {
const id = `update-network-param-proposal-${proposal.id}`;
return {
id: `update-network-param-proposal-${proposal.id}`,
intent: Intent.Warning,
content: <UpdateNetworkParameterToastContent proposal={proposal} />,
onClose: () => {
remove(id);
},
closeAfter: CLOSE_AFTER,
};
},
[remove]
);
return useOnUpdateNetworkParametersSubscription({
onData: ({ data }) => {
// note proposals is poorly named, it is actually a single proposal
const proposal = data.data?.proposals;
if (!proposal) return;
if (proposal.terms.change.__typename !== 'UpdateNetworkParameter') return;
// if one of the following states show a toast
if (PROPOSAL_STATES_TO_TOAST.includes(proposal.state)) {
setToast(fromProposal(proposal));
}
},
});
};
@@ -1,9 +1,9 @@
import { proposalsDataProvider } from '..';
import * as Schema from '@vegaprotocol/types';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { useMemo } from 'react';
import first from 'lodash/first';
import type { ProposalListFieldsFragment } from '..';
import { proposalsDataProvider } from '../proposals-data-provider';
import type { ProposalListFieldsFragment } from '../proposals-data-provider';
type UseUpdateProposalProps = {
id?: string;