feat: view proposed network changes (579) (#2444)
This commit is contained in:
parent
46955dd933
commit
65316075fa
@ -5,7 +5,7 @@ import {
|
||||
ProgressBar,
|
||||
ToastsContainer,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
useEthTransactionStore,
|
||||
useEthWithdrawApprovalsStore,
|
||||
@ -44,6 +44,7 @@ import {
|
||||
useLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { prepend0x } from '@vegaprotocol/smart-contracts';
|
||||
import { useUpdateNetworkParametersToasts } from '@vegaprotocol/governance';
|
||||
|
||||
const intentMap = {
|
||||
Default: Intent.Primary,
|
||||
@ -137,6 +138,7 @@ const EthTransactionDetails = ({ tx }: { tx: EthStoredTxState }) => {
|
||||
};
|
||||
|
||||
export const ToastsManager = () => {
|
||||
const updateNetworkParametersToasts = useUpdateNetworkParametersToasts();
|
||||
const vegaTransactions = useVegaTransactionStore((state) =>
|
||||
state.transactions.filter((transaction) => transaction?.dialogOpen)
|
||||
);
|
||||
@ -450,29 +452,20 @@ export const ToastsManager = () => {
|
||||
...compact(vegaTransactions).map(fromVegaTransaction),
|
||||
...compact(ethTransactions).map(fromEthTransaction),
|
||||
...compact(withdrawApprovals).map(fromWithdrawalApproval),
|
||||
...updateNetworkParametersToasts,
|
||||
],
|
||||
['createdBy']
|
||||
);
|
||||
}, [
|
||||
fromEthTransaction,
|
||||
fromVegaTransaction,
|
||||
fromWithdrawalApproval,
|
||||
ethTransactions,
|
||||
vegaTransactions,
|
||||
fromVegaTransaction,
|
||||
ethTransactions,
|
||||
fromEthTransaction,
|
||||
withdrawApprovals,
|
||||
fromWithdrawalApproval,
|
||||
updateNetworkParametersToasts,
|
||||
]);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
console.log([
|
||||
...vegaTransactions,
|
||||
...ethTransactions,
|
||||
...withdrawApprovals,
|
||||
]),
|
||||
[ethTransactions, vegaTransactions, withdrawApprovals]
|
||||
);
|
||||
useEffect(() => console.log(toasts), [toasts]);
|
||||
|
||||
return <ToastsContainer order="desc" toasts={toasts} />;
|
||||
};
|
||||
|
||||
|
@ -16,3 +16,30 @@ subscription ProposalEvent($partyId: ID!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment UpdateNetworkParameterFields on Proposal {
|
||||
id
|
||||
state
|
||||
datetime
|
||||
terms {
|
||||
enactmentDatetime
|
||||
change {
|
||||
... on UpdateNetworkParameter {
|
||||
networkParameter {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscription OnUpdateNetworkParameters {
|
||||
busEvents(types: [Proposal], batchSize: 0) {
|
||||
event {
|
||||
... on Proposal {
|
||||
...UpdateNetworkParameterFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,13 @@ export type ProposalEventSubscriptionVariables = Types.Exact<{
|
||||
|
||||
export type ProposalEventSubscription = { __typename?: 'Subscription', busEvents?: Array<{ __typename?: 'BusEvent', type: Types.BusEventType, event: { __typename?: 'AccountEvent' } | { __typename?: 'Asset' } | { __typename?: 'AuctionEvent' } | { __typename?: 'Deposit' } | { __typename?: 'LiquidityProvision' } | { __typename?: 'LossSocialization' } | { __typename?: 'MarginLevels' } | { __typename?: 'Market' } | { __typename?: 'MarketData' } | { __typename?: 'MarketEvent' } | { __typename?: 'MarketTick' } | { __typename?: 'NodeSignature' } | { __typename?: 'OracleSpec' } | { __typename?: 'Order' } | { __typename?: 'Party' } | { __typename?: 'PositionResolution' } | { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null } | { __typename?: 'RiskFactor' } | { __typename?: 'SettleDistressed' } | { __typename?: 'SettlePosition' } | { __typename?: 'TimeUpdate' } | { __typename?: 'Trade' } | { __typename?: 'TransactionResult' } | { __typename?: 'TransferResponses' } | { __typename?: 'Vote' } | { __typename?: 'Withdrawal' } }> | null };
|
||||
|
||||
export type UpdateNetworkParameterFieldsFragment = { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } } };
|
||||
|
||||
export type OnUpdateNetworkParametersSubscriptionVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type OnUpdateNetworkParametersSubscription = { __typename?: 'Subscription', busEvents?: Array<{ __typename?: 'BusEvent', event: { __typename?: 'AccountEvent' } | { __typename?: 'Asset' } | { __typename?: 'AuctionEvent' } | { __typename?: 'Deposit' } | { __typename?: 'LiquidityProvision' } | { __typename?: 'LossSocialization' } | { __typename?: 'MarginLevels' } | { __typename?: 'Market' } | { __typename?: 'MarketData' } | { __typename?: 'MarketEvent' } | { __typename?: 'MarketTick' } | { __typename?: 'NodeSignature' } | { __typename?: 'OracleSpec' } | { __typename?: 'Order' } | { __typename?: 'Party' } | { __typename?: 'PositionResolution' } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } } } | { __typename?: 'RiskFactor' } | { __typename?: 'SettleDistressed' } | { __typename?: 'SettlePosition' } | { __typename?: 'TimeUpdate' } | { __typename?: 'Trade' } | { __typename?: 'TransactionResult' } | { __typename?: 'TransferResponses' } | { __typename?: 'Vote' } | { __typename?: 'Withdrawal' } }> | null };
|
||||
|
||||
export const ProposalEventFieldsFragmentDoc = gql`
|
||||
fragment ProposalEventFields on Proposal {
|
||||
id
|
||||
@ -21,6 +28,24 @@ export const ProposalEventFieldsFragmentDoc = gql`
|
||||
errorDetails
|
||||
}
|
||||
`;
|
||||
export const UpdateNetworkParameterFieldsFragmentDoc = gql`
|
||||
fragment UpdateNetworkParameterFields on Proposal {
|
||||
id
|
||||
state
|
||||
datetime
|
||||
terms {
|
||||
enactmentDatetime
|
||||
change {
|
||||
... on UpdateNetworkParameter {
|
||||
networkParameter {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const ProposalEventDocument = gql`
|
||||
subscription ProposalEvent($partyId: ID!) {
|
||||
busEvents(partyId: $partyId, batchSize: 0, types: [Proposal]) {
|
||||
@ -55,4 +80,37 @@ export function useProposalEventSubscription(baseOptions: Apollo.SubscriptionHoo
|
||||
return Apollo.useSubscription<ProposalEventSubscription, ProposalEventSubscriptionVariables>(ProposalEventDocument, options);
|
||||
}
|
||||
export type ProposalEventSubscriptionHookResult = ReturnType<typeof useProposalEventSubscription>;
|
||||
export type ProposalEventSubscriptionResult = Apollo.SubscriptionResult<ProposalEventSubscription>;
|
||||
export type ProposalEventSubscriptionResult = Apollo.SubscriptionResult<ProposalEventSubscription>;
|
||||
export const OnUpdateNetworkParametersDocument = gql`
|
||||
subscription OnUpdateNetworkParameters {
|
||||
busEvents(types: [TimeUpdate], batchSize: 0) {
|
||||
event {
|
||||
... on Proposal {
|
||||
...UpdateNetworkParameterFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${UpdateNetworkParameterFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useOnUpdateNetworkParametersSubscription__
|
||||
*
|
||||
* To run a query within a React component, call `useOnUpdateNetworkParametersSubscription` and pass it any options that fit your needs.
|
||||
* When your component renders, `useOnUpdateNetworkParametersSubscription` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useOnUpdateNetworkParametersSubscription({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useOnUpdateNetworkParametersSubscription(baseOptions?: Apollo.SubscriptionHookOptions<OnUpdateNetworkParametersSubscription, OnUpdateNetworkParametersSubscriptionVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useSubscription<OnUpdateNetworkParametersSubscription, OnUpdateNetworkParametersSubscriptionVariables>(OnUpdateNetworkParametersDocument, options);
|
||||
}
|
||||
export type OnUpdateNetworkParametersSubscriptionHookResult = ReturnType<typeof useOnUpdateNetworkParametersSubscription>;
|
||||
export type OnUpdateNetworkParametersSubscriptionResult = Apollo.SubscriptionResult<OnUpdateNetworkParametersSubscription>;
|
@ -2,3 +2,4 @@ export * from './__generated__/Proposal';
|
||||
export * from './use-proposal-event';
|
||||
export * from './use-proposal-submit';
|
||||
export * from './use-update-proposal';
|
||||
export * from './use-update-network-paramaters-toasts';
|
||||
|
@ -0,0 +1,103 @@
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import { getDateTimeFormat, t } from '@vegaprotocol/react-helpers';
|
||||
import type { UpdateNetworkParameter } from '@vegaprotocol/types';
|
||||
import { ProposalStateMapping } from '@vegaprotocol/types';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import type { Toast } from '@vegaprotocol/ui-toolkit';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import compact from 'lodash/compact';
|
||||
import { useCallback } from 'react';
|
||||
import type { UpdateNetworkParameterFieldsFragment } from './__generated__/Proposal';
|
||||
import { useOnUpdateNetworkParametersSubscription } from './__generated__/Proposal';
|
||||
|
||||
const CLOSE_AFTER = 5000;
|
||||
type Proposal = UpdateNetworkParameterFieldsFragment;
|
||||
|
||||
const UpdateNetworkParameterToastContent = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
const tokenLink = useLinks(DApp.Token);
|
||||
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>
|
||||
<h3 className="font-bold">{title}</h3>
|
||||
<p className="italic">
|
||||
'
|
||||
{t(
|
||||
`Update ${change.networkParameter.key} to ${change.networkParameter.value}`
|
||||
)}
|
||||
'
|
||||
</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 = (): Toast[] => {
|
||||
const { proposalToasts, setToast, remove } = useToasts((store) => ({
|
||||
proposalToasts: store.toasts,
|
||||
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,
|
||||
render: () => (
|
||||
<UpdateNetworkParameterToastContent proposal={proposal} />
|
||||
),
|
||||
onClose: () => remove(id),
|
||||
closeAfter: CLOSE_AFTER,
|
||||
};
|
||||
},
|
||||
[remove]
|
||||
);
|
||||
|
||||
useOnUpdateNetworkParametersSubscription({
|
||||
onData: (options) => {
|
||||
const events = compact(options.data.data?.busEvents);
|
||||
if (!events || events.length === 0) return;
|
||||
const validProposals = events
|
||||
.filter(
|
||||
(ev) =>
|
||||
ev.event.__typename === 'Proposal' &&
|
||||
ev.event.terms.__typename === 'ProposalTerms' &&
|
||||
ev.event.terms.change.__typename === 'UpdateNetworkParameter' &&
|
||||
[
|
||||
ProposalState.STATE_DECLINED,
|
||||
ProposalState.STATE_ENACTED,
|
||||
ProposalState.STATE_OPEN,
|
||||
ProposalState.STATE_PASSED,
|
||||
].includes(ev.event.state)
|
||||
)
|
||||
.map((ev) => ev.event as Proposal);
|
||||
if (validProposals.length < 5) {
|
||||
validProposals.forEach((p) => setToast(fromProposal(p)));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return proposalToasts;
|
||||
};
|
@ -0,0 +1,132 @@
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useUpdateNetworkParametersToasts } from './use-update-network-paramaters-toasts';
|
||||
import type {
|
||||
UpdateNetworkParameterFieldsFragment,
|
||||
OnUpdateNetworkParametersSubscription,
|
||||
} from './__generated__/Proposal';
|
||||
import { OnUpdateNetworkParametersDocument } from './__generated__/Proposal';
|
||||
import waitForNextTick from 'flush-promises';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
const render = (mocks?: MockedResponse[]) => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={mocks}>{children}</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useUpdateNetworkParametersToasts(), { wrapper });
|
||||
};
|
||||
|
||||
const generateUpdateNetworkParametersProposal = (
|
||||
key: string,
|
||||
value: string,
|
||||
state: ProposalState = ProposalState.STATE_OPEN
|
||||
): UpdateNetworkParameterFieldsFragment => ({
|
||||
__typename: 'Proposal',
|
||||
id: Math.random().toString(),
|
||||
datetime: Math.random().toString(),
|
||||
state,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms',
|
||||
enactmentDatetime: '2022-12-09T14:40:38Z',
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
__typename: 'NetworkParameter',
|
||||
key,
|
||||
value,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mockedWrongEvent: MockedResponse<OnUpdateNetworkParametersSubscription> =
|
||||
{
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
__typename: 'Subscription',
|
||||
busEvents: [
|
||||
{
|
||||
__typename: 'BusEvent',
|
||||
event: {
|
||||
__typename: 'Asset',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockedEmptyEvent: MockedResponse<OnUpdateNetworkParametersSubscription> =
|
||||
{
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
__typename: 'Subscription',
|
||||
busEvents: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockedEvent: MockedResponse<OnUpdateNetworkParametersSubscription> = {
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
__typename: 'Subscription',
|
||||
busEvents: [
|
||||
{
|
||||
__typename: 'BusEvent',
|
||||
event: generateUpdateNetworkParametersProposal('abc.def', '123.456'),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
const { result: clearer } = renderHook(() =>
|
||||
useToasts((store) => store.removeAll)
|
||||
);
|
||||
act(() => clearer.current());
|
||||
};
|
||||
|
||||
describe('useUpdateNetworkParametersToasts', () => {
|
||||
beforeEach(clear);
|
||||
afterAll(clear);
|
||||
|
||||
it('returns toast for update network parameters bus event', async () => {
|
||||
const { waitForNextUpdate, result } = render([mockedEvent]);
|
||||
await act(async () => {
|
||||
waitForNextUpdate();
|
||||
await waitForNextTick();
|
||||
});
|
||||
expect(result.current.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not return toast for empty event', async () => {
|
||||
const { waitForNextUpdate, result } = render([mockedEmptyEvent]);
|
||||
await act(async () => {
|
||||
waitForNextUpdate();
|
||||
await waitForNextTick();
|
||||
});
|
||||
expect(result.current.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not return toast for wrong event', async () => {
|
||||
const { waitForNextUpdate, result } = render([mockedWrongEvent]);
|
||||
await act(async () => {
|
||||
waitForNextUpdate();
|
||||
await waitForNextTick();
|
||||
});
|
||||
expect(result.current.length).toBe(0);
|
||||
});
|
||||
});
|
@ -17,7 +17,7 @@ type ToastsStore = {
|
||||
/**
|
||||
* Adds a new toast or updates if id already exists.
|
||||
*/
|
||||
addOrUpdate: (toast: Toast) => void;
|
||||
setToast: (toast: Toast) => void;
|
||||
/**
|
||||
* Closes a toast
|
||||
*/
|
||||
@ -55,7 +55,7 @@ export const useToasts = create<ToastsStore>((set) => ({
|
||||
toasts: [],
|
||||
add: (toast) => set(add(toast)),
|
||||
update: (id, toastData) => set(update(id, toastData)),
|
||||
addOrUpdate: (toast: Toast) =>
|
||||
setToast: (toast: Toast) =>
|
||||
set((store) => {
|
||||
if (store.toasts.find((t) => t.id === toast.id)) {
|
||||
return update(toast.id, toast)(store);
|
||||
|
Loading…
Reference in New Issue
Block a user