feat: view proposed network changes (579) (#2444)

This commit is contained in:
Art 2022-12-22 11:49:02 +01:00 committed by GitHub
parent 46955dd933
commit 65316075fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 333 additions and 19 deletions

View File

@ -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} />;
};

View File

@ -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
}
}
}
}

View File

@ -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>;

View File

@ -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';

View File

@ -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;
};

View File

@ -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);
});
});

View File

@ -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);