ChangePass from TxComponent (#2520)
* ChangePass from TxComponent * Import as functional * Cleanups (vanity) * Use bundled node
This commit is contained in:
parent
ca749efff1
commit
31796b87f3
3
.github/workflows/pr-any.yml
vendored
3
.github/workflows/pr-any.yml
vendored
@ -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'
|
||||
|
||||
3
.github/workflows/push-master.yml
vendored
3
.github/workflows/push-master.yml
vendored
@ -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 }}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user