ChangePass from TxComponent (#2520)

* ChangePass from TxComponent

* Import as functional

* Cleanups (vanity)

* Use bundled node
This commit is contained in:
Jaco Greeff 2020-04-06 12:28:29 +02:00 committed by GitHub
parent ca749efff1
commit 31796b87f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 219 additions and 325 deletions

View File

@ -10,9 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: '12.x'
- name: ${{ matrix.step }}
run: |
yarn install --immutable | grep -v 'YN0013'

View File

@ -16,9 +16,6 @@ jobs:
- uses: actions/checkout@v1
with:
token: ${{ secrets.GH_PAT }}
- uses: actions/setup-node@v1
with:
node-version: '12.x'
- name: ${{ matrix.step }}
env:
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}

View File

@ -2,71 +2,83 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { I18nProps } from '@polkadot/react-components/types';
import React from 'react';
import { AddressRow, Button, Modal, Password, TxComponent } from '@polkadot/react-components';
import { ActionStatus } from '@polkadot/react-components/Status/types';
import React, { useCallback, useState } from 'react';
import { AddressRow, Button, Modal, Password } from '@polkadot/react-components';
import keyring from '@polkadot/ui-keyring';
import translate from '../../translate';
import { useTranslation } from '../../translate';
interface Props extends I18nProps {
interface Props {
className?: string;
address: string;
onClose: () => void;
}
interface State {
interface NewPass {
isNewValid: boolean;
isOldValid: boolean;
newPass: string;
}
interface OldPass {
isOldValid: boolean;
oldPass: string;
}
class ChangePass extends TxComponent<Props, State> {
public state: State = {
isNewValid: false,
isOldValid: false,
newPass: '',
oldPass: ''
};
function ChangePass ({ address, className, onClose }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [{ isNewValid, newPass }, setNewPass] = useState<NewPass>({ isNewValid: false, newPass: '' });
const [{ isOldValid, oldPass }, setOldPass] = useState<OldPass>({ isOldValid: false, oldPass: '' });
public render (): React.ReactNode {
const { t } = this.props;
const _onChangeNew = useCallback(
(newPass: string) =>
setNewPass({ isNewValid: keyring.isPassValid(newPass), newPass }),
[]
);
return (
<Modal
className='app--accounts-Modal'
header={t('Change account password')}
>
{this.renderContent()}
{this.renderButtons()}
</Modal>
);
}
const _onChangeOld = useCallback(
(oldPass: string) =>
setOldPass({ isOldValid: keyring.isPassValid(oldPass), oldPass }),
[]
);
private renderButtons (): React.ReactNode {
const { onClose, t } = this.props;
const { isNewValid, isOldValid } = this.state;
const _doChange = useCallback(
(): void => {
const account = address && keyring.getPair(address);
return (
<Modal.Actions onCancel={onClose}>
<Button
icon='sign-in'
isDisabled={!isNewValid || !isOldValid}
isPrimary
label={t('Change')}
onClick={this.doChange}
/>
</Modal.Actions>
);
}
if (!account) {
return;
}
private renderContent (): React.ReactNode {
const { address, t } = this.props;
const { isNewValid, isOldValid, newPass, oldPass } = this.state;
try {
if (!account.isLocked) {
account.lock();
}
return (
account.decodePkcs8(oldPass);
} catch (error) {
setOldPass((state: OldPass) => ({ ...state, isOldValid: false }));
return;
}
try {
keyring.encryptAccount(account, newPass);
} catch (error) {
setNewPass((state: NewPass) => ({ ...state, isNewValid: false }));
return;
}
onClose();
},
[address, newPass, oldPass, onClose]
);
return (
<Modal
className={`${className} app--accounts-Modal`}
header={t('Change account password')}
>
<Modal.Content>
<AddressRow
isInline
@ -79,7 +91,7 @@ class ChangePass extends TxComponent<Props, State> {
help={t('The existing account password as specified when this account was created or when it was last changed.')}
isError={!isOldValid}
label={t('your current password')}
onChange={this.onChangeOld}
onChange={_onChangeOld}
tabIndex={1}
value={oldPass}
/>
@ -87,84 +99,25 @@ class ChangePass extends TxComponent<Props, State> {
help={t('The new account password. Once set, all future account unlocks will be performed with this new password.')}
isError={!isNewValid}
label={t('your new password')}
onChange={this.onChangeNew}
onEnter={this.submit}
onChange={_onChangeNew}
onEnter={_doChange}
tabIndex={2}
value={newPass}
/>
</div>
</AddressRow>
</Modal.Content>
);
}
private doChange = (): void => {
const { address, onClose, t } = this.props;
const { newPass, oldPass } = this.state;
const status: Partial<ActionStatus> = {
action: 'changePassword'
};
try {
const account = address && keyring.getPair(address);
if (!account) {
status.message = t(`No keypair found for this address ${address}`);
return;
}
try {
if (!account.isLocked) {
account.lock();
}
account.decodePkcs8(oldPass);
} catch (error) {
this.setState({ isOldValid: false });
status.message = error.message;
return;
}
try {
keyring.encryptAccount(account, newPass);
status.account = address;
status.status = 'success';
status.message = t('password changed');
} catch (error) {
this.setState({ isNewValid: false });
status.status = 'error';
status.message = error.message;
return;
}
} catch (error) {
status.message = error.message;
return;
}
onClose();
}
private onChangeNew = (newPass: string): void => {
this.setState({
isNewValid: this.validatePass(newPass),
newPass
});
}
private onChangeOld = (oldPass: string): void => {
this.setState({
isOldValid: this.validatePass(oldPass),
oldPass
});
}
private validatePass (password: string): boolean {
return keyring.isPassValid(password);
}
<Modal.Actions onCancel={onClose}>
<Button
icon='sign-in'
isDisabled={!isNewValid || !isOldValid}
isPrimary
label={t('Change')}
onClick={_doChange}
/>
</Modal.Actions>
</Modal>
);
}
export default translate(ChangePass);
export default React.memo(ChangePass);

View File

@ -3,62 +3,108 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeyringPair$Json } from '@polkadot/keyring/types';
import { I18nProps } from '@polkadot/react-components/types';
import { ActionStatus } from '@polkadot/react-components/Status/types';
import { ModalProps } from '../../types';
import React from 'react';
import { AddressRow, Button, InputAddress, InputFile, Modal, Password, TxComponent } from '@polkadot/react-components';
import React, { useCallback, useState } from 'react';
import { AddressRow, Button, InputAddress, InputFile, Modal, Password } from '@polkadot/react-components';
import { isHex, isObject, u8aToString } from '@polkadot/util';
import keyring from '@polkadot/ui-keyring';
import translate from '../../translate';
import { useTranslation } from '../../translate';
interface Props extends ModalProps, I18nProps {}
interface Props extends ModalProps {
className?: string;
}
interface State {
interface FileState {
address: string | null;
isFileValid: boolean;
isPassValid: boolean;
json: KeyringPair$Json | null;
}
interface PassState {
isPassValid: boolean;
password: string;
}
class Import extends TxComponent<Props, State> {
public state: State = {
address: null,
isFileValid: false,
isPassValid: false,
json: null,
password: ''
};
const acceptedFormats = ['application/json', 'text/plain'].join(', ');
public render (): React.ReactNode {
const { onClose, t } = this.props;
const { isFileValid, isPassValid } = this.state;
return (
<Modal header={t('Add via backup file')}>
{this.renderInput()}
<Modal.Actions onCancel={onClose}>
<Button
icon='sync'
isDisabled={!isFileValid || !isPassValid}
isPrimary
label={t('Restore')}
onClick={this.onSave}
/>
</Modal.Actions>
</Modal>
function parseFile (file: Uint8Array): FileState {
try {
const json = JSON.parse(u8aToString(file));
const publicKey = keyring.decodeAddress(json.address, true);
const address = keyring.encodeAddress(publicKey);
const isFileValid = publicKey.length === 32 && isHex(json.encoded) && isObject(json.meta) && (
Array.isArray(json.encoding.content)
? json.encoding.content[0] === 'pkcs8'
: json.encoding.content === 'pkcs8'
);
return { address, isFileValid, json };
} catch (error) {
console.error(error);
}
private renderInput (): React.ReactNode {
const { t } = this.props;
const { address, isFileValid, isPassValid, json, password } = this.state;
const acceptedFormats = ['application/json', 'text/plain'].join(', ');
return { address: null, isFileValid: false, json: null };
}
return (
function Import ({ className, onClose, onStatusChange }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [{ address, isFileValid, json }, setFile] = useState<FileState>({ address: null, isFileValid: false, json: null });
const [{ isPassValid, password }, setPass] = useState<PassState>({ isPassValid: false, password: '' });
const _onChangeFile = useCallback(
(file: Uint8Array): void =>
setFile(parseFile(file)),
[]
);
const _onChangePass = useCallback(
(password: string): void =>
setPass({ isPassValid: keyring.isPassValid(password), password }),
[]
);
const _onSave = useCallback(
(): void => {
if (!json) {
return;
}
const status: Partial<ActionStatus> = { action: 'restore' };
try {
const pair = keyring.restoreAccount(json, password);
const { address } = pair;
status.status = pair ? 'success' : 'error';
status.account = address;
status.message = t('account restored');
InputAddress.setLastValue('account', address);
} catch (error) {
setPass((state: PassState) => ({ ...state, isPassValid: false }));
status.status = 'error';
status.message = error.message;
console.error(error);
}
onStatusChange(status as ActionStatus);
if (status.status !== 'error') {
onClose();
}
},
[json, onClose, onStatusChange, password, t]
);
return (
<Modal
className={className}
header={t('Add via backup file')}
>
<Modal.Content>
<AddressRow
defaultName={isFileValid && json ? json.meta.name : null}
@ -71,7 +117,7 @@ class Import extends TxComponent<Props, State> {
help={t('Select the JSON key file that was downloaded when you created the account. This JSON file contains your private key encrypted with your password.')}
isError={!isFileValid}
label={t('backup file')}
onChange={this.onChangeFile}
onChange={_onChangeFile}
withLabel
/>
<Password
@ -80,81 +126,23 @@ class Import extends TxComponent<Props, State> {
help={t('Type the password chosen at the account creation. It was used to encrypt your account\'s private key in the backup file.')}
isError={!isPassValid}
label={t('password')}
onChange={this.onChangePass}
onEnter={this.submit}
onChange={_onChangePass}
onEnter={_onSave}
value={password}
/>
</AddressRow>
</Modal.Content>
);
}
private onChangeFile = (file: Uint8Array): void => {
try {
const json = JSON.parse(u8aToString(file));
const publicKey = keyring.decodeAddress(json.address, true);
const address = keyring.encodeAddress(publicKey);
const isFileValid = publicKey.length === 32 && isHex(json.encoded) && isObject(json.meta) && (
Array.isArray(json.encoding.content)
? json.encoding.content[0] === 'pkcs8'
: json.encoding.content === 'pkcs8'
);
this.setState({
address,
isFileValid,
json
});
} catch (error) {
this.setState({
address: null,
isFileValid: false,
json: null
});
console.error(error);
}
}
private onChangePass = (password: string): void => {
this.setState({
isPassValid: keyring.isPassValid(password),
password
});
}
private onSave = (): void => {
const { onClose, onStatusChange, t } = this.props;
const { json, password } = this.state;
if (!json) {
return;
}
const status: Partial<ActionStatus> = { action: 'restore' };
try {
const pair = keyring.restoreAccount(json, password);
const { address } = pair;
status.status = pair ? 'success' : 'error';
status.account = address;
status.message = t('account restored');
InputAddress.setLastValue('account', address);
} catch (error) {
this.setState({ isPassValid: false });
status.status = 'error';
status.message = error.message;
console.error(error);
}
onStatusChange(status as ActionStatus);
if (status.status !== 'error') {
onClose();
}
}
<Modal.Actions onCancel={onClose}>
<Button
icon='sync'
isDisabled={!isFileValid || !isPassValid}
isPrimary
label={t('Restore')}
onClick={_onSave}
/>
</Modal.Actions>
</Modal>
);
}
export default translate(Import);
export default React.memo(Import);

View File

@ -67,74 +67,11 @@ class VanityApp extends TxComponent<Props, State> {
}
public render (): React.ReactNode {
const { className, onStatusChange } = this.props;
const { createSeed, type } = this.state;
const { className, onStatusChange, t } = this.props;
const { createSeed, elapsed, isMatchValid, isRunning, keyCount, match, matches, type, withCase } = this.state;
return (
<div className={className}>
{this.renderOptions()}
{this.renderButtons()}
{this.renderStats()}
{this.renderMatches()}
{createSeed && (
<CreateModal
onClose={this.closeCreate}
onStatusChange={onStatusChange}
seed={createSeed}
type={type}
/>
)}
</div>
);
}
private renderButtons (): React.ReactNode {
const { t } = this.props;
const { isMatchValid, isRunning } = this.state;
return (
<Button.Group>
<Button
icon={
isRunning
? 'stop'
: 'sign-in'
}
isDisabled={!isMatchValid}
label={
isRunning
? t('Stop generation')
: t('Start generation')
}
onClick={this.toggleStart}
/>
</Button.Group>
);
}
private renderMatches (): React.ReactNode {
const { matches } = this.state;
return (
<div className='vanity--App-matches'>
{matches.map((match): React.ReactNode => (
<Match
{...match}
key={match.address}
onCreateToggle={this.onCreateToggle}
onRemove={this.onRemove}
/>
))}
</div>
);
}
private renderOptions (): React.ReactNode {
const { t } = this.props;
const { isMatchValid, isRunning, match, type, withCase } = this.state;
return (
<>
<div className='ui--row'>
<Input
autoFocus
@ -167,29 +104,51 @@ class VanityApp extends TxComponent<Props, State> {
options={uiSettings.availableCryptos}
/>
</div>
</>
);
}
private renderStats (): React.ReactNode {
const { t } = this.props;
const { elapsed, keyCount } = this.state;
if (!keyCount) {
return null;
}
const secs = elapsed / 1000;
return (
<div className='vanity--App-stats'>
{t('Evaluated {{count}} keys in {{elapsed}}s ({{avg}} keys/s)', {
replace: {
avg: (keyCount / secs).toFixed(3),
count: keyCount,
elapsed: secs.toFixed(2)
}
})}
<Button.Group>
<Button
icon={
isRunning
? 'stop'
: 'sign-in'
}
isDisabled={!isMatchValid}
label={
isRunning
? t('Stop generation')
: t('Start generation')
}
onClick={this.toggleStart}
/>
</Button.Group>
{keyCount !== 0 && (
<div className='vanity--App-stats'>
{t('Evaluated {{count}} keys in {{elapsed}}s ({{avg}} keys/s)', {
replace: {
avg: (keyCount / (elapsed / 1000)).toFixed(3),
count: keyCount,
elapsed: (elapsed / 1000).toFixed(2)
}
})}
</div>
)}
<div className='vanity--App-matches'>
{matches.map((match): React.ReactNode => (
<Match
{...match}
key={match.address}
onCreateToggle={this.onCreateToggle}
onRemove={this.onRemove}
/>
))}
</div>
{createSeed && (
<CreateModal
onClose={this.closeCreate}
onStatusChange={onStatusChange}
seed={createSeed}
type={type}
/>
)}
</div>
);
}