index.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. // Copyright 2017-2020 @polkadot/app-claims authors & contributors
  2. // This software may be modified and distributed under the terms
  3. // of the Apache-2.0 license. See the LICENSE file for details.
  4. import { Option } from '@polkadot/types';
  5. import { EcdsaSignature, EthereumAddress, StatementKind } from '@polkadot/types/interfaces';
  6. import React, { useState, useCallback, useEffect } from 'react';
  7. import { Trans } from 'react-i18next';
  8. import styled from 'styled-components';
  9. import CopyToClipboard from 'react-copy-to-clipboard';
  10. import { Button, Card, Columar, Column, Input, InputAddress, Tooltip } from '@polkadot/react-components';
  11. import { useApi, useCall } from '@polkadot/react-hooks';
  12. import { TokenUnit } from '@polkadot/react-components/InputNumber';
  13. import { u8aToHex, u8aToString } from '@polkadot/util';
  14. import { decodeAddress } from '@polkadot/util-crypto';
  15. import { useTranslation } from './translate';
  16. import { recoverFromJSON, getStatement } from './util';
  17. import AttestDisplay from './Attest';
  18. import ClaimDisplay from './Claim';
  19. import Statement from './Statement';
  20. import Warning from './Warning';
  21. export { default as useCounter } from './useCounter';
  22. enum Step {
  23. Account = 0,
  24. ETHAddress = 1,
  25. Sign = 2,
  26. Claim = 3,
  27. }
  28. const PRECLAIMS_LOADING = 'PRECLAIMS_LOADING';
  29. // FIXME no embedded components (hossible to tweak)
  30. const Payload = styled.pre`
  31. cursor: copy;
  32. font-family: monospace;
  33. border: 1px dashed #c2c2c2;
  34. background: #f2f2f2;
  35. padding: 1rem;
  36. width: 100%;
  37. margin: 1rem 0;
  38. white-space: normal;
  39. word-break: break-all;
  40. `;
  41. const Signature = styled.textarea`
  42. font-family: monospace;
  43. padding: 1rem;
  44. border: 1px solid rgba(34, 36, 38, 0.15);
  45. border-radius: 0.25rem;
  46. margin: 1rem 0;
  47. resize: none;
  48. width: 100%;
  49. &::placeholder {
  50. color: rgba(0, 0, 0, 0.5);
  51. }
  52. &::-ms-input-placeholder {
  53. color: rgba(0, 0, 0, 0.5);
  54. }
  55. &:-ms-input-placeholder {
  56. color: rgba(0, 0, 0, 0.5);
  57. }
  58. `;
  59. function ClaimsApp (): React.ReactElement {
  60. const [didCopy, setDidCopy] = useState(false);
  61. const [ethereumAddress, setEthereumAddress] = useState<string | undefined | null>(null);
  62. const [signature, setSignature] = useState<EcdsaSignature | null>(null);
  63. const [step, setStep] = useState<Step>(Step.Account);
  64. const [accountId, setAccountId] = useState<string | null>(null);
  65. const { api, systemChain } = useApi();
  66. const { t } = useTranslation();
  67. // This preclaimEthereumAddress holds the result of `api.query.claims.preclaims`:
  68. // - an `EthereumAddress` when there's a preclaim
  69. // - null if no preclaim
  70. // - `PRECLAIMS_LOADING` if we're fetching the results
  71. const [preclaimEthereumAddress, setPreclaimEthereumAddress] = useState<string | null | undefined | typeof PRECLAIMS_LOADING>(PRECLAIMS_LOADING);
  72. const isPreclaimed = !!preclaimEthereumAddress && preclaimEthereumAddress !== PRECLAIMS_LOADING;
  73. // Everytime we change account, reset everything, and check if the accountId
  74. // has a preclaim.
  75. useEffect(() => {
  76. if (!accountId) {
  77. return;
  78. }
  79. setStep(Step.Account);
  80. setEthereumAddress(null);
  81. setPreclaimEthereumAddress(PRECLAIMS_LOADING);
  82. if (!api.query.claims || !api.query.claims.preclaims) {
  83. return setPreclaimEthereumAddress(null);
  84. }
  85. api.query.claims
  86. .preclaims<Option<EthereumAddress>>(accountId)
  87. .then((preclaim): void => {
  88. const address = preclaim.unwrapOr(null)?.toString();
  89. setEthereumAddress(address);
  90. setPreclaimEthereumAddress(address);
  91. })
  92. .catch((): void => setPreclaimEthereumAddress(null));
  93. }, [accountId, api.query.claims, api.query.claims.preclaims]);
  94. // Old claim process used `api.tx.claims.claim`, and didn't have attest
  95. const isOldClaimProcess = !api.tx.claims.claimAttest;
  96. useEffect(() => {
  97. if (didCopy) {
  98. setTimeout((): void => {
  99. setDidCopy(false);
  100. }, 1000);
  101. }
  102. }, [didCopy]);
  103. const goToStepAccount = useCallback(() => {
  104. setStep(Step.Account);
  105. }, []);
  106. const goToStepSign = useCallback(() => {
  107. setStep(Step.Sign);
  108. }, []);
  109. const goToStepClaim = useCallback(() => {
  110. setStep(Step.Claim);
  111. }, []);
  112. // Depending on the account, decide which step to show.
  113. const handleAccountStep = useCallback(() => {
  114. if (isPreclaimed) {
  115. goToStepClaim();
  116. } else if (ethereumAddress || isOldClaimProcess) {
  117. goToStepSign();
  118. } else {
  119. setStep(Step.ETHAddress);
  120. }
  121. }, [ethereumAddress, goToStepClaim, goToStepSign, isPreclaimed, isOldClaimProcess]);
  122. const onChangeSignature = useCallback((event: React.SyntheticEvent<Element>) => {
  123. const { value: signatureJson } = event.target as HTMLInputElement;
  124. const { ethereumAddress, signature } = recoverFromJSON(signatureJson);
  125. setEthereumAddress(ethereumAddress?.toString());
  126. setSignature(signature);
  127. }, []);
  128. const onChangeEthereumAddress = useCallback((value: string) => {
  129. // FIXME We surely need a better check than just a trim
  130. setEthereumAddress(value.trim());
  131. }, []);
  132. const onCopy = useCallback(() => {
  133. setDidCopy(true);
  134. }, []);
  135. // If it's 1/ not preclaimed and 2/ not the old claiming process, fetch the
  136. // statement kind to sign.
  137. const statementKind = useCall<StatementKind | null>(!isPreclaimed && !isOldClaimProcess && !!ethereumAddress && api.query.claims.signing, [ethereumAddress], {
  138. transform: (option: Option<StatementKind>) => option.unwrapOr(null)
  139. });
  140. const statementSentence = getStatement(systemChain, statementKind)?.sentence || '';
  141. const prefix = u8aToString(api.consts.claims.prefix.toU8a(true));
  142. const payload = accountId
  143. ? `${prefix}${u8aToHex(decodeAddress(accountId), -1, false)}${statementSentence}`
  144. : '';
  145. return (
  146. <main>
  147. <header />
  148. {!isOldClaimProcess && <Warning />}
  149. <h1>
  150. <Trans>Claim your <em>{TokenUnit.abbr}</em> tokens</Trans>
  151. </h1>
  152. <Columar>
  153. <Column>
  154. <Card withBottomMargin>
  155. <h3>{t<string>('1. Select your {{chain}} account', {
  156. replace: {
  157. chain: systemChain
  158. }
  159. })}</h3>
  160. <InputAddress
  161. defaultValue={accountId}
  162. help={t<string>('The account you want to claim to.')}
  163. label={t<string>('claim to account')}
  164. onChange={setAccountId}
  165. type='all'
  166. />
  167. {(step === Step.Account) && (
  168. <Button.Group>
  169. <Button
  170. icon='sign-in-alt'
  171. isDisabled={preclaimEthereumAddress === PRECLAIMS_LOADING}
  172. label={preclaimEthereumAddress === PRECLAIMS_LOADING
  173. ? t<string>('Loading')
  174. : t<string>('Continue')
  175. }
  176. onClick={handleAccountStep}
  177. />
  178. </Button.Group>
  179. )}
  180. </Card>
  181. {
  182. // We need to know the ethereuem address only for the new process
  183. // to be able to know the statement kind so that the users can sign it
  184. (step >= Step.ETHAddress && !isPreclaimed && !isOldClaimProcess) && (
  185. <Card withBottomMargin>
  186. <h3>{t<string>('2. Enter the ETH address from the sale.')}</h3>
  187. <Input
  188. autoFocus
  189. className='full'
  190. help={t<string>('The the Ethereum address you used during the pre-sale (starting by "0x")')}
  191. label={t<string>('Pre-sale ethereum address')}
  192. onChange={onChangeEthereumAddress}
  193. value={ethereumAddress || ''}
  194. />
  195. {(step === Step.ETHAddress) && (
  196. <Button.Group>
  197. <Button
  198. icon='sign-in-alt'
  199. isDisabled={!ethereumAddress}
  200. label={t<string>('Continue')}
  201. onClick={goToStepSign}
  202. />
  203. </Button.Group>
  204. )}
  205. </Card>
  206. )}
  207. {(step >= Step.Sign && !isPreclaimed) && (
  208. <Card>
  209. <h3>{t<string>('{{step}}. Sign with your ETH address', { replace: { step: isOldClaimProcess ? '2' : '3' } })}</h3>
  210. {!isOldClaimProcess && (
  211. <Statement
  212. kind={statementKind}
  213. systemChain={systemChain}
  214. />
  215. )}
  216. <div>{t<string>('Copy the following string and sign it with the Ethereum account you used during the pre-sale in the wallet of your choice, using the string as the payload, and then paste the transaction signature object below:')}</div>
  217. <CopyToClipboard
  218. onCopy={onCopy}
  219. text={payload}
  220. >
  221. <Payload
  222. data-for='tx-payload'
  223. data-tip
  224. >
  225. {payload}
  226. </Payload>
  227. </CopyToClipboard>
  228. <Tooltip
  229. place='right'
  230. text={didCopy ? t<string>('copied') : t<string>('click to copy')}
  231. trigger='tx-payload'
  232. />
  233. <Signature
  234. onChange={onChangeSignature}
  235. placeholder={`{\n "address": "0x ...",\n "msg": "${prefix}:...",\n "sig": "0x ...",\n "version": "2"\n}`}
  236. rows={10}
  237. />
  238. {(step === Step.Sign) && (
  239. <Button.Group>
  240. <Button
  241. icon='sign-in-alt'
  242. isDisabled={!accountId || !signature}
  243. label={t<string>('Confirm claim')}
  244. onClick={goToStepClaim}
  245. />
  246. </Button.Group>
  247. )}
  248. </Card>
  249. )}
  250. </Column>
  251. <Column showEmptyText={false}>
  252. {(step >= Step.Claim) && (
  253. isPreclaimed
  254. ? <AttestDisplay
  255. accountId={accountId}
  256. ethereumAddress={ethereumAddress}
  257. onSuccess={goToStepAccount}
  258. statementKind={statementKind}
  259. systemChain={systemChain}
  260. />
  261. : <ClaimDisplay
  262. accountId={accountId}
  263. ethereumAddress={ethereumAddress}
  264. ethereumSignature={signature}
  265. isOldClaimProcess={isOldClaimProcess}
  266. onSuccess={goToStepAccount}
  267. statementKind={statementKind}
  268. systemChain={systemChain}
  269. />
  270. )}
  271. </Column>
  272. </Columar>
  273. </main>
  274. );
  275. }
  276. export default React.memo(ClaimsApp);