Import.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. // Copyright 2017-2020 @polkadot/app-accounts 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 { KeyringPair$Json } from '@polkadot/keyring/types';
  5. import { ActionStatus } from '@polkadot/react-components/Status/types';
  6. import { ModalProps } from '../../types';
  7. import React, { useCallback, useState } from 'react';
  8. import { AddressRow, Button, InputAddress, InputFile, Modal, Password } from '@polkadot/react-components';
  9. import { isObject, u8aToString } from '@polkadot/util';
  10. import keyring from '@polkadot/ui-keyring';
  11. import { isPasswordValid } from '@polkadot/joy-utils/functions/accounts';
  12. import { useTranslation } from '../../translate';
  13. interface Props extends ModalProps {
  14. className?: string;
  15. onClose: () => void;
  16. onStatusChange: (status: ActionStatus) => void;
  17. }
  18. interface FileState {
  19. address: string | null;
  20. isFileValid: boolean;
  21. json: KeyringPair$Json | null;
  22. }
  23. interface PassState {
  24. isPassValid: boolean;
  25. password: string;
  26. }
  27. const acceptedFormats = ['application/json', 'text/plain'].join(', ');
  28. function parseFile (file: Uint8Array): FileState {
  29. try {
  30. const json = JSON.parse(u8aToString(file)) as KeyringPair$Json;
  31. const publicKey = keyring.decodeAddress(json.address, true);
  32. const address = keyring.encodeAddress(publicKey);
  33. const isFileValid = publicKey.length === 32 && !!json.encoded && isObject(json.meta) && (
  34. Array.isArray(json.encoding.content)
  35. ? json.encoding.content[0] === 'pkcs8'
  36. : json.encoding.content === 'pkcs8'
  37. );
  38. return { address, isFileValid, json };
  39. } catch (error) {
  40. console.error(error);
  41. }
  42. return { address: null, isFileValid: false, json: null };
  43. }
  44. function Import ({ className = '', onClose, onStatusChange }: Props): React.ReactElement<Props> {
  45. const { t } = useTranslation();
  46. const [isBusy, setIsBusy] = useState(false);
  47. const [{ address, isFileValid, json }, setFile] = useState<FileState>({ address: null, isFileValid: false, json: null });
  48. const [{ isPassValid, password }, setPass] = useState<PassState>({ isPassValid: true, password: '' });
  49. const _onChangeFile = useCallback(
  50. (file: Uint8Array) => setFile(parseFile(file)),
  51. []
  52. );
  53. const _onChangePass = useCallback(
  54. (password: string) => setPass({ isPassValid: isPasswordValid(password), password }),
  55. []
  56. );
  57. const _onSave = useCallback(
  58. (): void => {
  59. if (!json) {
  60. return;
  61. }
  62. setIsBusy(true);
  63. setTimeout((): void => {
  64. const status: Partial<ActionStatus> = { action: 'restore' };
  65. try {
  66. const pair = keyring.restoreAccount(json, password);
  67. const { address } = pair;
  68. status.status = pair ? 'success' : 'error';
  69. status.account = address;
  70. status.message = t<string>('account restored');
  71. InputAddress.setLastValue('account', address);
  72. } catch (error) {
  73. setPass((state: PassState) => ({ ...state, isPassValid: false }));
  74. status.status = 'error';
  75. status.message = (error as Error).message;
  76. console.error(error);
  77. }
  78. setIsBusy(false);
  79. onStatusChange(status as ActionStatus);
  80. if (status.status !== 'error') {
  81. onClose();
  82. }
  83. }, 0);
  84. },
  85. [json, onClose, onStatusChange, password, t]
  86. );
  87. return (
  88. <Modal
  89. className={className}
  90. header={t<string>('Add via backup file')}
  91. size='large'
  92. >
  93. <Modal.Content>
  94. <Modal.Columns>
  95. <Modal.Column>
  96. <AddressRow
  97. defaultName={(isFileValid && json?.meta.name as string) || null}
  98. noDefaultNameOpacity
  99. value={isFileValid && address ? address : null}
  100. />
  101. </Modal.Column>
  102. </Modal.Columns>
  103. <Modal.Columns>
  104. <Modal.Column>
  105. <InputFile
  106. accept={acceptedFormats}
  107. className='full'
  108. help={t<string>('Select the JSON key file that was downloaded when you created the account. This JSON file contains your private key encrypted with your password.')}
  109. isError={!isFileValid}
  110. label={t<string>('backup file')}
  111. onChange={_onChangeFile}
  112. withLabel
  113. />
  114. </Modal.Column>
  115. <Modal.Column>
  116. <p>{t<string>('Supply a backed-up JSON file, encrypted with your account-specific password.')}</p>
  117. </Modal.Column>
  118. </Modal.Columns>
  119. <Modal.Columns>
  120. <Modal.Column>
  121. <Password
  122. autoFocus
  123. className='full'
  124. help={t<string>('Type the password chosen at the account creation. It was used to encrypt your account\'s private key in the backup file.')}
  125. isError={!isPassValid}
  126. label={t<string>('password')}
  127. onChange={_onChangePass}
  128. onEnter={_onSave}
  129. value={password}
  130. />
  131. </Modal.Column>
  132. <Modal.Column>
  133. <p>{t<string>('The password previously used to encrypt this account.')}</p>
  134. </Modal.Column>
  135. </Modal.Columns>
  136. </Modal.Content>
  137. <Modal.Actions onCancel={onClose}>
  138. <Button
  139. icon='sync'
  140. isBusy={isBusy}
  141. isDisabled={!isFileValid || !isPassValid}
  142. label={t<string>('Restore')}
  143. onClick={_onSave}
  144. />
  145. </Modal.Actions>
  146. </Modal>
  147. );
  148. }
  149. export default React.memo(Import);