Feat/1513: Vote buttons using subscription and dialog for tx (#1943)

* Feat/1513: Vote buttons using subscription and dialog for tx

* Feat/1513: Staking journey using dialog

* Feat/1513: Cleaning up unused imports

* Feat/1513: Format tweak
This commit is contained in:
Sam Keen 2022-11-07 17:46:57 +00:00 committed by GitHub
parent e31422ae82
commit d026c9bdd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 425 additions and 308 deletions

View File

@ -302,7 +302,7 @@
"stakingHasAssociated": "You have {{tokens}} $VEGA tokens associated.",
"stakingAssociateMoreButton": "Associate more $VEGA tokens with wallet",
"stakingDisassociateButton": "Disassociate $VEGA tokens from wallet",
"stakingConfirm": "Open you wallet app to confirm",
"stakingConfirm": "Open your wallet app to confirm",
"associateButton": "Associate $VEGA tokens with wallet",
"nodeQueryFailed": "Could not get data for validator {{node}}",
"stakeAddPendingTitle": "Adding {{amount}} $VEGA to validator {{node}}",

View File

@ -1,17 +1,9 @@
import { captureException, captureMessage } from '@sentry/minimal';
import { captureMessage } from '@sentry/minimal';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { VoteValue } from '@vegaprotocol/types';
import { useEffect, useMemo, useState } from 'react';
export type Vote = {
value: VoteValue;
datetime: string;
party: { id: string };
};
export type Votes = Array<Vote | null>;
export enum VoteState {
NotCast = 'NotCast',
Yes = 'Yes',
@ -21,6 +13,14 @@ export enum VoteState {
Failed = 'Failed',
}
export type Vote = {
value: VoteValue;
datetime: string;
party: { id: string };
};
export type Votes = Array<Vote | null>;
export function getUserVote(pubkey: string, yesVotes?: Votes, noVotes?: Votes) {
const yesVote = yesVotes?.find((v) => v && v.party.id === pubkey);
const noVote = noVotes?.find((v) => v && v.party.id === pubkey);
@ -42,7 +42,7 @@ export function useUserVote(
yesVotes: Votes | null,
noVotes: Votes | null
) {
const { pubKey, sendTx } = useVegaWallet();
const { pubKey } = useVegaWallet();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [timeout, setTimeoutValue] = useState<any>(null);
const yes = useMemo(() => yesVotes || [], [yesVotes]);
@ -87,37 +87,8 @@ export function useUserVote(
return () => clearTimeout(timeout);
}, [timeout, voteState]);
/**
* Casts a vote using the users connected wallet
*/
async function castVote(value: VoteValue) {
if (!proposalId || !pubKey) return;
const previousVoteState = voteState;
setVoteState(VoteState.Requested);
try {
const res = await sendTx(pubKey, {
voteSubmission: {
value: value,
proposalId,
},
});
if (res === null) {
setVoteState(previousVoteState);
} else {
setVoteState(VoteState.Pending);
}
// Now await vote via poll in parent component
} catch (err) {
setVoteState(VoteState.Failed);
captureException(err);
}
}
return {
voteState,
castVote,
userVote,
voteDatetime: userVote ? new Date(userVote.datetime) : null,
};

View File

@ -2,26 +2,29 @@ import { render, screen, fireEvent } from '@testing-library/react';
import BigNumber from 'bignumber.js';
import { VoteButtons } from './vote-buttons';
import { VoteState } from './use-user-vote';
import { ProposalState, VoteValue } from '@vegaprotocol/types';
import { ProposalState } from '@vegaprotocol/types';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { mockWalletContext } from '../../test-helpers/mocks';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { MockedProvider } from '@apollo/react-testing';
describe('Vote buttons', () => {
it('should render successfully', () => {
const { baseElement } = render(
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.NotCast}
castVote={jest.fn()}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(1)}
/>
</VegaWalletContext.Provider>
<MockedProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.NotCast}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
proposalId={null}
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(1)}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</AppStateProvider>
);
expect(baseElement).toBeTruthy();
@ -30,17 +33,19 @@ describe('Vote buttons', () => {
it('should explain that voting is closed if the proposal is not open', () => {
render(
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.NotCast}
castVote={jest.fn()}
voteDatetime={null}
proposalState={ProposalState.STATE_PASSED}
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(1)}
/>
</VegaWalletContext.Provider>
<MockedProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.NotCast}
voteDatetime={null}
proposalState={ProposalState.STATE_PASSED}
proposalId={null}
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(1)}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</AppStateProvider>
);
expect(screen.getByText('Voting has ended.')).toBeTruthy();
@ -59,17 +64,19 @@ describe('Vote buttons', () => {
render(
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletNoPubKeyContext}>
<VoteButtons
voteState={VoteState.NotCast}
castVote={jest.fn()}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(1)}
/>
</VegaWalletContext.Provider>
<MockedProvider>
<VegaWalletContext.Provider value={mockWalletNoPubKeyContext}>
<VoteButtons
voteState={VoteState.NotCast}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
proposalId={null}
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(1)}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</AppStateProvider>
);
@ -79,17 +86,19 @@ describe('Vote buttons', () => {
it('should tell the user they need tokens if their current stake is 0', () => {
render(
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.NotCast}
castVote={jest.fn()}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(0)}
/>
</VegaWalletContext.Provider>
<MockedProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.NotCast}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
proposalId={null}
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(0)}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</AppStateProvider>
);
expect(
@ -100,17 +109,19 @@ describe('Vote buttons', () => {
it('should tell the user of the minimum requirements if they have some, but not enough tokens', () => {
render(
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.NotCast}
castVote={jest.fn()}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(1)}
/>
</VegaWalletContext.Provider>
<MockedProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.NotCast}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
proposalId={null}
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(1)}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</AppStateProvider>
);
expect(
@ -120,148 +131,47 @@ describe('Vote buttons', () => {
).toBeTruthy();
});
it('should display vote requested', () => {
render(
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.Requested}
castVote={jest.fn()}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(10)}
/>
</VegaWalletContext.Provider>
</AppStateProvider>
);
expect(screen.getByTestId('vote-requested')).toBeInTheDocument();
});
it('should display vote pending', () => {
render(
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.Pending}
castVote={jest.fn()}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(10)}
/>
</VegaWalletContext.Provider>
</AppStateProvider>
);
expect(screen.getByTestId('vote-pending')).toBeInTheDocument();
});
it('should show you voted if vote state is correct, and if the proposal is still open, it will display a change vote button', () => {
render(
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.Yes}
castVote={jest.fn()}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(10)}
/>
</VegaWalletContext.Provider>
<MockedProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.Yes}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
proposalId={null}
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(10)}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</AppStateProvider>
);
expect(screen.getByTestId('you-voted')).toBeInTheDocument();
expect(screen.getByTestId('change-vote-button')).toBeInTheDocument();
});
it('should display vote failure', () => {
render(
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.Failed}
castVote={jest.fn()}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(10)}
/>
</VegaWalletContext.Provider>
</AppStateProvider>
);
expect(screen.getByTestId('vote-failure')).toBeInTheDocument();
});
it('should cast yes vote when vote-for button is clicked', () => {
const castVote = jest.fn();
render(
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.NotCast}
castVote={castVote}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(10)}
/>
</VegaWalletContext.Provider>
</AppStateProvider>
);
expect(screen.getByTestId('vote-buttons')).toBeInTheDocument();
const button = screen.getByTestId('vote-for');
fireEvent.click(button);
expect(castVote).toHaveBeenCalledWith(VoteValue.VALUE_YES);
});
it('should cast no vote when vote-against button is clicked', () => {
const castVote = jest.fn();
render(
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.NotCast}
castVote={castVote}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(10)}
/>
</VegaWalletContext.Provider>
</AppStateProvider>
);
expect(screen.getByTestId('vote-buttons')).toBeInTheDocument();
const button = screen.getByTestId('vote-against');
fireEvent.click(button);
expect(castVote).toHaveBeenCalledWith(VoteValue.VALUE_NO);
});
it('should allow you to change your vote', () => {
const castVote = jest.fn();
render(
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.No}
castVote={castVote}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(10)}
/>
</VegaWalletContext.Provider>
<MockedProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.No}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
proposalId={null}
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(10)}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</AppStateProvider>
);
fireEvent.click(screen.getByTestId('change-vote-button'));
fireEvent.click(screen.getByTestId('vote-for'));
expect(castVote).toHaveBeenCalledWith(VoteValue.VALUE_YES);
expect(screen.getByTestId('vote-buttons')).toBeInTheDocument();
});
});

View File

@ -2,29 +2,29 @@ import { gql, useQuery } from '@apollo/client';
import { format } from 'date-fns';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { AsyncRenderer, Button, ButtonLink } from '@vegaprotocol/ui-toolkit';
import { addDecimal, toBigNum } from '@vegaprotocol/react-helpers';
import { useVoteSubmit } from '@vegaprotocol/governance';
import { ProposalState, VoteValue } from '@vegaprotocol/types';
import {
AppStateActionType,
useAppState,
} from '../../../../contexts/app-state/app-state-context';
import { BigNumber } from '../../../../lib/bignumber';
import { DATE_FORMAT_LONG } from '../../../../lib/date-formats';
import { VoteState } from './use-user-vote';
import { ProposalMinRequirements, ProposalUserAction } from '../shared';
import { VoteTransactionDialog } from './vote-transaction-dialog';
import type {
VoteButtonsQuery as VoteButtonsQueryResult,
VoteButtonsQueryVariables,
} from './__generated__/VoteButtonsQuery';
import { VoteState } from './use-user-vote';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { ProposalState, VoteValue } from '@vegaprotocol/types';
import { ProposalUserAction } from '../shared';
import { AsyncRenderer, Button, ButtonLink } from '@vegaprotocol/ui-toolkit';
import { ProposalMinRequirements } from '../shared';
import { addDecimal, toBigNum } from '@vegaprotocol/react-helpers';
interface VoteButtonsContainerProps {
voteState: VoteState | null;
castVote: (vote: VoteValue) => void;
voteDatetime: Date | null;
proposalId: string | null;
proposalState: ProposalState;
minVoterBalance: string | null;
spamProtectionMinTokens: string | null;
@ -74,9 +74,9 @@ interface VoteButtonsProps extends VoteButtonsContainerProps {
export const VoteButtons = ({
voteState,
castVote,
voteDatetime,
proposalState,
proposalId,
currentStakeAvailable,
minVoterBalance,
spamProtectionMinTokens,
@ -84,6 +84,7 @@ export const VoteButtons = ({
const { t } = useTranslation();
const { appDispatch } = useAppState();
const { pubKey } = useVegaWallet();
const { submit, Dialog } = useVoteSubmit();
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
@ -161,7 +162,7 @@ export const VoteButtons = ({
function submitVote(vote: VoteValue) {
setChangeVote(false);
castVote(vote);
submit(vote, proposalId);
}
// Should only render null for a split second while initial vote state
@ -174,64 +175,56 @@ export const VoteButtons = ({
return <div>{cantVoteUI}</div>;
}
if (voteState === VoteState.Requested) {
return <p data-testid="vote-requested">{t('voteRequested')}...</p>;
}
if (voteState === VoteState.Pending) {
return <p data-testid="vote-pending">{t('votePending')}...</p>;
}
// If voted show current vote info`
if (
!changeVote &&
(voteState === VoteState.Yes || voteState === VoteState.No)
) {
const className =
voteState === VoteState.Yes ? 'text-success' : 'text-danger';
return (
<p data-testid="you-voted">
<span>{t('youVoted')}:</span>{' '}
<span className={className}>{t(`voteState_${voteState}`)}</span>{' '}
{voteDatetime ? (
<span>{format(voteDatetime, DATE_FORMAT_LONG)}. </span>
) : null}
{proposalVotable ? (
<ButtonLink
data-testid="change-vote-button"
onClick={() => {
setChangeVote(true);
}}
>
{t('changeVote')}
</ButtonLink>
) : null}
</p>
);
}
if (!changeVote && voteState === VoteState.Failed) {
return <p data-testid="vote-failure">{t('voteError')}</p>;
}
return (
<div className="flex gap-4" data-testid="vote-buttons">
<div className="flex-1">
<Button
data-testid="vote-for"
onClick={() => submitVote(VoteValue.VALUE_YES)}
>
{t('voteFor')}
</Button>
</div>
<div className="flex-1">
<Button
data-testid="vote-against"
onClick={() => submitVote(VoteValue.VALUE_NO)}
>
{t('voteAgainst')}
</Button>
</div>
</div>
<>
{changeVote || (voteState === VoteState.NotCast && proposalVotable) ? (
<div className="flex gap-4" data-testid="vote-buttons">
<div className="flex-1">
<Button
data-testid="vote-for"
onClick={() => submitVote(VoteValue.VALUE_YES)}
>
{t('voteFor')}
</Button>
</div>
<div className="flex-1">
<Button
data-testid="vote-against"
onClick={() => submitVote(VoteValue.VALUE_NO)}
>
{t('voteAgainst')}
</Button>
</div>
</div>
) : (
(voteState === VoteState.Yes || voteState === VoteState.No) && (
<p data-testid="you-voted">
<span>{t('youVoted')}:</span>{' '}
<span
className={
voteState === VoteState.Yes ? 'text-success' : 'text-danger'
}
>
{t(`voteState_${voteState}`)}
</span>{' '}
{voteDatetime ? (
<span>{format(voteDatetime, DATE_FORMAT_LONG)}. </span>
) : null}
{proposalVotable ? (
<ButtonLink
data-testid="change-vote-button"
onClick={() => {
setChangeVote(true);
voteState = VoteState.NotCast;
}}
>
{t('changeVote')}
</ButtonLink>
) : null}
</p>
)
)}
<VoteTransactionDialog voteState={voteState} TransactionDialog={Dialog} />
</>
);
};

View File

@ -39,12 +39,11 @@ export const VoteDetails = ({
} = useVoteInformation({ proposal });
const { t } = useTranslation();
const { voteState, voteDatetime, castVote } = useUserVote(
const { voteState, voteDatetime } = useUserVote(
proposal.id,
proposal.votes.yes.votes,
proposal.votes.no.votes
);
const defaultDecimals = 2;
return (
@ -164,9 +163,9 @@ export const VoteDetails = ({
<h3 className="text-xl mb-2">{t('yourVote')}</h3>
<VoteButtonsContainer
voteState={voteState}
castVote={castVote}
voteDatetime={voteDatetime}
proposalState={proposal.state}
proposalId={proposal.id}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={spamProtectionMinTokens}
className="flex"

View File

@ -0,0 +1,40 @@
import { t } from '@vegaprotocol/react-helpers';
import { VoteState } from './use-user-vote';
import type { DialogProps } from '@vegaprotocol/wallet';
interface VoteTransactionDialogProps {
voteState: VoteState;
TransactionDialog: (props: DialogProps) => JSX.Element;
}
const dialogTitle = (voteState: VoteState): string | undefined => {
switch (voteState) {
case VoteState.Requested:
return t('voteRequested');
case VoteState.Pending:
return t('votePending');
default:
return undefined;
}
};
export const VoteTransactionDialog = ({
voteState,
TransactionDialog,
}: VoteTransactionDialogProps) => {
// Render a custom message if the voting fails otherwise
// pass undefined so that the default vega transaction dialog UI gets used
const customMessage =
voteState === VoteState.Failed ? <p>{t('voteError')}</p> : undefined;
return (
<div data-testid="vote-transaction-dialog">
<TransactionDialog
title={dialogTitle(voteState)}
content={{
Complete: customMessage,
}}
/>
</div>
);
};

View File

@ -1,4 +1,4 @@
import { Callout, Intent } from '@vegaprotocol/ui-toolkit';
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
interface StakeFailureProps {
@ -8,12 +8,16 @@ interface StakeFailureProps {
export const StakeFailure = ({ nodeName }: StakeFailureProps) => {
const { t } = useTranslation();
return (
<Callout intent={Intent.Danger} title={t('Something went wrong')}>
<Dialog
intent={Intent.Danger}
title={t('Something went wrong')}
open={true}
>
<p>
{t('stakeFailed', {
node: nodeName,
})}
</p>
</Callout>
</Dialog>
);
};

View File

@ -1,4 +1,4 @@
import { Callout, Loader } from '@vegaprotocol/ui-toolkit';
import { Dialog, Loader } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import { Actions } from './staking-form';
import type { StakeAction } from './staking-form';
@ -22,8 +22,8 @@ export const StakePending = ({
: t('stakeRemovePendingTitle', titleArgs);
return (
<Callout icon={<Loader size="small" />} title={title}>
<Dialog icon={<Loader size="small" />} title={title} open={true}>
<p>{t('timeForConfirmation')}</p>
</Callout>
</Dialog>
);
};

View File

@ -1,4 +1,4 @@
import { Callout, Intent } from '@vegaprotocol/ui-toolkit';
import { Dialog, Icon, Intent } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import Routes from '../../routes';
@ -30,7 +30,12 @@ export const StakeSuccess = ({
: t('stakeRemoveNowSuccessMessage');
return (
<Callout iconName="tick" intent={Intent.Success} title={title}>
<Dialog
icon={<Icon name="tick" />}
intent={Intent.Success}
title={title}
open={true}
>
<div>
<p>{message}</p>
<p>
@ -39,6 +44,6 @@ export const StakeSuccess = ({
</Link>
</p>
</div>
</Callout>
</Dialog>
);
};

View File

@ -17,7 +17,7 @@ import { StakePending } from './stake-pending';
import { StakeSuccess } from './stake-success';
import {
ButtonLink,
Callout,
Dialog,
FormGroup,
Intent,
Radio,
@ -200,9 +200,13 @@ export const StakingForm = ({
return <StakeFailure nodeName={nodeName} />;
} else if (formState === FormState.Requested) {
return (
<Callout title="Confirm transaction in wallet" intent={Intent.Warning}>
<Dialog
title="Confirm transaction in wallet"
intent={Intent.Warning}
open={true}
>
<p>{t('stakingConfirm')}</p>
</Callout>
</Dialog>
);
} else if (formState === FormState.Pending) {
return <StakePending action={action} amount={amount} nodeName={nodeName} />;

View File

@ -1,2 +1,3 @@
export * from './proposals-hooks';
export * from './proposals-queries';
export * from './voting-hooks';

View File

@ -0,0 +1,18 @@
fragment VoteEventFields on Vote {
proposalId
value
datetime
}
subscription VoteEvent($partyId: ID!) {
busEvents(partyId: $partyId, batchSize: 0, types: [Vote]) {
type
event {
... on Vote {
proposalId
value
datetime
}
}
}
}

View File

@ -0,0 +1,58 @@
import { Schema as Types } from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type VoteEventFieldsFragment = { __typename?: 'Vote', proposalId: string, value: Types.VoteValue, datetime: string };
export type VoteEventSubscriptionVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type VoteEventSubscription = { __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' } | { __typename?: 'RiskFactor' } | { __typename?: 'SettleDistressed' } | { __typename?: 'SettlePosition' } | { __typename?: 'TimeUpdate' } | { __typename?: 'Trade' } | { __typename?: 'TransactionResult' } | { __typename?: 'TransferResponses' } | { __typename?: 'Vote', proposalId: string, value: Types.VoteValue, datetime: string } | { __typename?: 'Withdrawal' } }> | null };
export const VoteEventFieldsFragmentDoc = gql`
fragment VoteEventFields on Vote {
proposalId
value
datetime
}
`;
export const VoteEventDocument = gql`
subscription VoteEvent($partyId: ID!) {
busEvents(partyId: $partyId, batchSize: 0, types: [Vote]) {
type
event {
... on Vote {
proposalId
value
datetime
}
}
}
}
`;
/**
* __useVoteEventSubscription__
*
* To run a query within a React component, call `useVoteEventSubscription` and pass it any options that fit your needs.
* When your component renders, `useVoteEventSubscription` 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 } = useVoteEventSubscription({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function useVoteEventSubscription(baseOptions: Apollo.SubscriptionHookOptions<VoteEventSubscription, VoteEventSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<VoteEventSubscription, VoteEventSubscriptionVariables>(VoteEventDocument, options);
}
export type VoteEventSubscriptionHookResult = ReturnType<typeof useVoteEventSubscription>;
export type VoteEventSubscriptionResult = Apollo.SubscriptionResult<VoteEventSubscription>;

View File

@ -0,0 +1 @@
export * from './use-vote-submit';

View File

@ -0,0 +1,62 @@
import { useApolloClient } from '@apollo/client';
import { useCallback, useEffect, useRef } from 'react';
import { VoteEventDocument } from './__generated___/VoteSubsciption';
import type { Subscription } from 'zen-observable-ts';
import type { VegaTxState } from '@vegaprotocol/wallet';
import type {
VoteEventFieldsFragment,
VoteEventSubscription,
VoteEventSubscriptionVariables,
} from './__generated___/VoteSubsciption';
export const useVoteEvent = (transaction: VegaTxState) => {
const client = useApolloClient();
const subRef = useRef<Subscription | null>(null);
const waitForVoteEvent = useCallback(
(
proposalId: string,
partyId: string,
callback: (vote: VoteEventFieldsFragment) => void
) => {
subRef.current = client
.subscribe<VoteEventSubscription, VoteEventSubscriptionVariables>({
query: VoteEventDocument,
variables: { partyId },
})
.subscribe(({ data }) => {
if (!data?.busEvents?.length) {
return;
}
const matchingVoteEvent = data.busEvents.find((e) => {
if (e.event.__typename !== 'Vote') {
return false;
}
return e.event.proposalId === proposalId;
});
if (
matchingVoteEvent &&
matchingVoteEvent.event.__typename === 'Vote'
) {
callback(matchingVoteEvent.event);
subRef.current?.unsubscribe();
}
});
},
[client]
);
useEffect(() => {
if (!transaction.dialogOpen) {
subRef.current?.unsubscribe();
}
return () => {
subRef.current?.unsubscribe();
};
}, [transaction.dialogOpen]);
return waitForVoteEvent;
};

View File

@ -0,0 +1,51 @@
import { useCallback, useState } from 'react';
import * as Sentry from '@sentry/react';
import { useVegaTransaction, useVegaWallet } from '@vegaprotocol/wallet';
import { useVoteEvent } from './use-vote-event';
import type { VoteValue } from '@vegaprotocol/types';
import type { VoteEventFieldsFragment } from './__generated___/VoteSubsciption';
export const useVoteSubmit = () => {
const { pubKey } = useVegaWallet();
const { send, transaction, setComplete, Dialog } = useVegaTransaction();
const waitForVoteEvent = useVoteEvent(transaction);
const [finalizedVote, setFinalizedVote] =
useState<VoteEventFieldsFragment | null>(null);
const submit = useCallback(
async (voteValue: VoteValue, proposalId: string | null) => {
if (!pubKey || !voteValue || !proposalId) {
return;
}
setFinalizedVote(null);
try {
const res = await send(pubKey, {
voteSubmission: {
value: voteValue,
proposalId,
},
});
if (res) {
waitForVoteEvent(proposalId, pubKey, (v) => {
setFinalizedVote(v);
setComplete();
});
}
} catch (e) {
Sentry.captureException(e);
}
},
[pubKey, send, setComplete, waitForVoteEvent]
);
return {
transaction,
finalizedVote,
Dialog,
submit,
};
};