VoteForm.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import BN from 'bn.js';
  2. import uuid from 'uuid/v4';
  3. import React from 'react';
  4. import { Message, Table } from 'semantic-ui-react';
  5. import { AppProps, I18nProps } from '@polkadot/react-components/types';
  6. import { ApiProps } from '@polkadot/react-api/types';
  7. import { withCalls, withMulti } from '@polkadot/react-api/with';
  8. import { AccountId, Balance } from '@polkadot/types/interfaces';
  9. import { Button, Input, Labelled } from '@polkadot/react-components/index';
  10. import { SubmittableResult } from '@polkadot/api';
  11. import { formatBalance } from '@polkadot/util';
  12. import translate from './translate';
  13. import { hashVote } from './utils';
  14. import { queryToProp, ZERO, getUrlParam, nonEmptyStr } from '@polkadot/joy-utils/index';
  15. import TxButton from '@polkadot/joy-utils/TxButton';
  16. import InputStake from '@polkadot/joy-utils/InputStake';
  17. import CandidatePreview from "./CandidatePreview";
  18. import { MyAccountProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
  19. import MembersDropdown from "@polkadot/joy-utils/MembersDropdown";
  20. import { saveVote, NewVote } from './myVotesStore';
  21. import { TxFailedCallback } from '@polkadot/react-components/Status/types';
  22. // TODO use a crypto-prooven generator instead of UUID 4.
  23. function randomSalt () {
  24. return uuid().replace(/-/g, '');
  25. }
  26. // AppsProps is needed to get a location from the route.
  27. type Props = AppProps & ApiProps & I18nProps & MyAccountProps & {
  28. applicantId?: string | null,
  29. minVotingStake?: Balance,
  30. applicants?: AccountId[],
  31. location?: any,
  32. };
  33. type State = {
  34. applicantId?: string | null,
  35. stake?: BN,
  36. salt?: string,
  37. isStakeValid?: boolean,
  38. isFormSubmitted: boolean
  39. };
  40. class Component extends React.PureComponent<Props, State> {
  41. constructor (props: Props) {
  42. super(props);
  43. let { applicantId, location } = this.props;
  44. applicantId = applicantId ? applicantId : getUrlParam(location, 'applicantId');
  45. this.state = {
  46. applicantId,
  47. stake: ZERO,
  48. salt: randomSalt(),
  49. isFormSubmitted: false
  50. };
  51. }
  52. render () {
  53. const { myAddress } = this.props;
  54. const { applicantId, stake, salt, isStakeValid, isFormSubmitted } = this.state;
  55. const isFormValid = nonEmptyStr(applicantId) && isStakeValid;
  56. const hashedVote = hashVote(applicantId, salt);
  57. const buildNewVote = (): Partial<NewVote> => ({
  58. voterId: myAddress,
  59. applicantId: applicantId || undefined,
  60. stake: (stake || ZERO).toString(),
  61. salt: salt,
  62. hash: hashedVote || undefined
  63. });
  64. return (
  65. <>{isFormSubmitted
  66. // Summary of submitted vote:
  67. ? <div>
  68. <Message info>
  69. Your vote has been sent
  70. </Message>
  71. <Table celled selectable compact definition className='SealedVoteTable'>
  72. <Table.Body>
  73. <Table.Row>
  74. <Table.Cell>Applicant</Table.Cell>
  75. <Table.Cell>
  76. { applicantId && <CandidatePreview accountId={applicantId}/> }
  77. </Table.Cell>
  78. </Table.Row>
  79. <Table.Row>
  80. <Table.Cell>Stake</Table.Cell>
  81. <Table.Cell>{formatBalance(stake)}</Table.Cell>
  82. </Table.Row>
  83. <Table.Row>
  84. <Table.Cell>Salt</Table.Cell>
  85. <Table.Cell><code>{salt}</code></Table.Cell>
  86. </Table.Row>
  87. <Table.Row>
  88. <Table.Cell>Hashed vote</Table.Cell>
  89. <Table.Cell><code>{hashedVote}</code></Table.Cell>
  90. </Table.Row>
  91. </Table.Body>
  92. </Table>
  93. <Labelled style={{ marginTop: '.5rem' }}>
  94. <Button
  95. size='large'
  96. label='Submit another vote'
  97. onClick={this.resetForm}
  98. icon=''
  99. />
  100. </Labelled>
  101. </div>
  102. // New vote form:
  103. : <div>
  104. <div className='ui--row'>
  105. <MembersDropdown
  106. onChange={ (event, data) => this.onChangeApplicant(data.value as string) }
  107. accounts={this.props.applicants || []}
  108. value={applicantId || ''}
  109. placeholder="Select an applicant you support"
  110. />
  111. </div>
  112. <InputStake
  113. min={this.minStake()}
  114. isValid={isStakeValid}
  115. onChange={this.onChangeStake}
  116. />
  117. <div className='ui--row'>
  118. <Input
  119. className='large'
  120. isDisabled={true}
  121. label='Random salt:'
  122. value={salt}
  123. onChange={this.onChangeSalt}
  124. />
  125. <div className='medium' style={{ margin: '.5rem' }}>
  126. <Button onClick={this.newRandomSalt} icon=''>Generate</Button>
  127. <Message compact warning size='tiny' content='You need to remember this salt!' />
  128. </div>
  129. </div>
  130. <div className='ui--row'>
  131. <Input
  132. isDisabled={true}
  133. label='Hashed vote:'
  134. value={hashedVote}
  135. />
  136. </div>
  137. <Labelled style={{ marginTop: '.5rem' }}>
  138. <TxButton
  139. size='large'
  140. isDisabled={!isFormValid}
  141. label='Submit my vote'
  142. params={[hashedVote, stake]}
  143. tx='councilElection.vote'
  144. txStartCb={this.onFormSubmitted}
  145. txFailedCb={this.onTxFailed}
  146. txSuccessCb={(txResult: SubmittableResult) => this.onTxSuccess(buildNewVote() as NewVote, txResult)}
  147. />
  148. </Labelled>
  149. </div>}
  150. </>
  151. );
  152. }
  153. private resetForm = (): void => {
  154. this.onChangeStake(ZERO);
  155. this.newRandomSalt();
  156. this.setState({ isFormSubmitted: false });
  157. }
  158. private onFormSubmitted = (): void => {
  159. this.setState({ isFormSubmitted: true });
  160. }
  161. private onTxFailed: TxFailedCallback = (_txResult: SubmittableResult | null): void => {
  162. // TODO Possible UX improvement: tell a user that his vote hasn't been accepted.
  163. }
  164. private onTxSuccess = (vote: NewVote, txResult: SubmittableResult): void => {
  165. let hasVotedEvent = false;
  166. txResult.events.forEach((event, i) => {
  167. const { section, method } = event.event;
  168. if (section === 'councilElection' && method === 'Voted') {
  169. hasVotedEvent = true;
  170. }
  171. });
  172. if (hasVotedEvent) {
  173. saveVote(vote);
  174. this.setState({ isFormSubmitted: true });
  175. }
  176. }
  177. private newRandomSalt = (): void => {
  178. this.setState({ salt: randomSalt() });
  179. }
  180. private minStake = (): BN => {
  181. return this.props.minVotingStake || new BN(1);
  182. }
  183. private onChangeStake = (stake?: BN) => {
  184. const isStakeValid = stake && stake.gte(this.minStake());
  185. this.setState({ stake, isStakeValid });
  186. }
  187. private onChangeApplicant = (applicantId?: string | null) => {
  188. this.setState({ applicantId });
  189. }
  190. private onChangeSalt = (salt?: string) => {
  191. // TODO check that salt is unique by checking Substrate store.
  192. this.setState({ salt });
  193. }
  194. }
  195. export default withMulti(
  196. Component,
  197. translate,
  198. withOnlyMembers,
  199. withCalls<Props>(
  200. queryToProp('query.councilElection.minVotingStake'),
  201. queryToProp('query.councilElection.applicants')
  202. )
  203. );