Account backup & restore functionality (#333)
* Account JSON backup & restore * import isObject from correct package * Fix isLocked logic * Add "or"
This commit is contained in:
parent
e089a6c395
commit
818dfb2498
@ -11,6 +11,8 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@polkadot/ui-app": "^0.20.19"
|
||||
"@polkadot/ui-app": "^0.20.19",
|
||||
"@types/file-saver": "^1.3.0",
|
||||
"file-saver": "^1.3.8"
|
||||
}
|
||||
}
|
||||
|
||||
150
packages/app-accounts/src/Backup.tsx
Normal file
150
packages/app-accounts/src/Backup.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
// Copyright 2017-2018 @polkadot/app-accounts authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { KeyringPair } from '@polkadot/util-keyring/types';
|
||||
import { I18nProps } from '@polkadot/ui-app/types';
|
||||
|
||||
import FileSaver from 'file-saver';
|
||||
import React from 'react';
|
||||
import Button from '@polkadot/ui-app/Button';
|
||||
import Modal from '@polkadot/ui-app/Modal';
|
||||
import Password from '@polkadot/ui-app/Password';
|
||||
import AddressSummary from '@polkadot/ui-app/AddressSummary';
|
||||
|
||||
import translate from './translate';
|
||||
|
||||
type Props = I18nProps & {
|
||||
onClose: () => void,
|
||||
pair: KeyringPair
|
||||
};
|
||||
|
||||
type State = {
|
||||
isPassValid: boolean,
|
||||
password: string
|
||||
};
|
||||
|
||||
class Backup extends React.PureComponent<Props, State> {
|
||||
state: State;
|
||||
|
||||
constructor (props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isPassValid: false,
|
||||
password: ''
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Modal
|
||||
size='tiny'
|
||||
dimmer='inverted'
|
||||
open
|
||||
>
|
||||
{this.renderContent()}
|
||||
{this.renderButtons()}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
renderButtons () {
|
||||
const { onClose, t } = this.props;
|
||||
const { isPassValid } = this.state;
|
||||
|
||||
return (
|
||||
<Modal.Actions>
|
||||
<Button.Group>
|
||||
<Button
|
||||
isNegative
|
||||
onClick={onClose}
|
||||
text={t('backup.close', {
|
||||
defaultValue: 'Cancel'
|
||||
})}
|
||||
/>
|
||||
<Button.Or />
|
||||
<Button
|
||||
isDisabled={!isPassValid}
|
||||
isPrimary
|
||||
onClick={this.doBackup}
|
||||
text={t('backup.download', {
|
||||
defaultValue: 'Download'
|
||||
})}
|
||||
/>
|
||||
</Button.Group>
|
||||
</Modal.Actions>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent () {
|
||||
const { pair, t } = this.props;
|
||||
const { isPassValid, password } = this.state;
|
||||
|
||||
return [
|
||||
<Modal.Header key='header'>
|
||||
{t('backup.header', {
|
||||
defaultValue: 'Backup account'
|
||||
})}
|
||||
</Modal.Header>,
|
||||
<Modal.Content key='content'>
|
||||
<AddressSummary
|
||||
className='accounts--Modal-Address'
|
||||
value={pair.address()}
|
||||
/>
|
||||
<div className='ui--row'>
|
||||
<Password
|
||||
isError={!isPassValid}
|
||||
label={t('backup.password', {
|
||||
defaultValue: 'unlock account using the password'
|
||||
})}
|
||||
onChange={this.onChangePass}
|
||||
tabIndex={0}
|
||||
value={password}
|
||||
/>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
];
|
||||
}
|
||||
|
||||
private doBackup = (): void => {
|
||||
const { onClose, pair } = this.props;
|
||||
const { password } = this.state;
|
||||
|
||||
if (!pair) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!pair.isLocked()) {
|
||||
pair.lock();
|
||||
}
|
||||
|
||||
pair.decodePkcs8(password);
|
||||
} catch (error) {
|
||||
this.setState({ isPassValid: false });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.stringify(pair.toJson(password));
|
||||
const blob = new Blob([json], { type: 'application/json; charset=utf-8' });
|
||||
|
||||
FileSaver.saveAs(blob, `${pair.address()}.json`);
|
||||
} catch (error) {
|
||||
this.setState({ isPassValid: false });
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
}
|
||||
|
||||
private onChangePass = (password: string) => {
|
||||
this.setState({
|
||||
isPassValid: password.length > 0 && password.length <= 32,
|
||||
password
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(Backup);
|
||||
@ -12,6 +12,7 @@ import Input from '@polkadot/ui-app/Input';
|
||||
import InputAddress from '@polkadot/ui-app/InputAddress';
|
||||
import keyring from '@polkadot/ui-keyring/index';
|
||||
|
||||
import Backup from './Backup';
|
||||
import ChangePass from './ChangePass';
|
||||
import Forgetting from './Forgetting';
|
||||
import translate from './translate';
|
||||
@ -24,6 +25,7 @@ type Props = I18nProps & {
|
||||
type State = {
|
||||
current: KeyringPair | null,
|
||||
editedName: string,
|
||||
isBackupOpen: boolean,
|
||||
isEdited: boolean,
|
||||
isForgetOpen: boolean,
|
||||
isPasswordOpen: boolean
|
||||
@ -66,6 +68,14 @@ class Editor extends React.PureComponent<Props, State> {
|
||||
})}
|
||||
/>
|
||||
<Button.Group.Divider />
|
||||
<Button
|
||||
isDisabled={isEdited}
|
||||
onClick={this.toggleBackup}
|
||||
text={t('editor.backup', {
|
||||
defaultValue: 'Backup'
|
||||
})}
|
||||
/>
|
||||
<Button.Or />
|
||||
<Button
|
||||
isDisabled={isEdited}
|
||||
onClick={this.togglePass}
|
||||
@ -139,7 +149,7 @@ class Editor extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
renderModals () {
|
||||
const { current, isForgetOpen, isPasswordOpen } = this.state;
|
||||
const { current, isBackupOpen, isForgetOpen, isPasswordOpen } = this.state;
|
||||
|
||||
if (!current) {
|
||||
return null;
|
||||
@ -148,6 +158,16 @@ class Editor extends React.PureComponent<Props, State> {
|
||||
const address = current.address();
|
||||
const modals = [];
|
||||
|
||||
if (isBackupOpen) {
|
||||
modals.push(
|
||||
<Backup
|
||||
key='modal-backup-account'
|
||||
pair={current}
|
||||
onClose={this.toggleBackup}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isForgetOpen) {
|
||||
modals.push(
|
||||
<Forgetting
|
||||
@ -178,6 +198,7 @@ class Editor extends React.PureComponent<Props, State> {
|
||||
editedName: current
|
||||
? current.getMeta().name || ''
|
||||
: '',
|
||||
isBackupOpen: false,
|
||||
isEdited: false,
|
||||
isForgetOpen: false,
|
||||
isPasswordOpen: false
|
||||
@ -204,6 +225,7 @@ class Editor extends React.PureComponent<Props, State> {
|
||||
return {
|
||||
current,
|
||||
editedName,
|
||||
isBackupOpen: false,
|
||||
isEdited,
|
||||
isForgetOpen: false,
|
||||
isPasswordOpen: false
|
||||
@ -253,6 +275,14 @@ class Editor extends React.PureComponent<Props, State> {
|
||||
} as State);
|
||||
}
|
||||
|
||||
toggleBackup = (): void => {
|
||||
this.setState(
|
||||
({ isBackupOpen }: State) => ({
|
||||
isBackupOpen: !isBackupOpen
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
toggleForget = (): void => {
|
||||
this.setState(
|
||||
({ isForgetOpen }: State) => ({
|
||||
|
||||
168
packages/app-accounts/src/Restore.tsx
Normal file
168
packages/app-accounts/src/Restore.tsx
Normal file
@ -0,0 +1,168 @@
|
||||
// Copyright 2017-2018 @polkadot/app-accounts authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { KeyringPair$Json } from '@polkadot/util-keyring/types';
|
||||
import { I18nProps } from '@polkadot/ui-app/types';
|
||||
|
||||
import React from 'react';
|
||||
import AddressSummary from '@polkadot/ui-app/AddressSummary';
|
||||
import Button from '@polkadot/ui-app/Button';
|
||||
import InputFile from '@polkadot/ui-app/InputFile';
|
||||
import { InputAddress } from '@polkadot/ui-app/InputAddress';
|
||||
import Password from '@polkadot/ui-app/Password';
|
||||
import createPair from '@polkadot/util-keyring/pair';
|
||||
import decodeAddress from '@polkadot/util-keyring/address/decode';
|
||||
import isHex from '@polkadot/util/is/hex';
|
||||
import isObject from '@polkadot/util/is/object';
|
||||
import hexToU8a from '@polkadot/util/hex/toU8a';
|
||||
import u8aToUtf8 from '@polkadot/util/u8a/toUtf8';
|
||||
import keyring from '@polkadot/ui-keyring/index';
|
||||
|
||||
import translate from './translate';
|
||||
|
||||
type Props = I18nProps & {
|
||||
onBack: () => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
isFileValid: boolean,
|
||||
isPassValid: boolean,
|
||||
json: KeyringPair$Json | null,
|
||||
password: string
|
||||
};
|
||||
|
||||
class Restore extends React.PureComponent<Props, State> {
|
||||
state: State;
|
||||
|
||||
constructor (props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
json: null,
|
||||
isFileValid: false,
|
||||
isPassValid: false,
|
||||
password: ''
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.props;
|
||||
const { isFileValid, isPassValid, json } = this.state;
|
||||
|
||||
return (
|
||||
<div className='accounts--Restore'>
|
||||
<div className='ui--grid'>
|
||||
<AddressSummary
|
||||
className='shrink'
|
||||
value={
|
||||
isFileValid && json
|
||||
? json.address
|
||||
: null
|
||||
}
|
||||
/>
|
||||
{this.renderInput()}
|
||||
</div>
|
||||
<Button.Group>
|
||||
<Button
|
||||
isDisabled={!isFileValid || !isPassValid}
|
||||
isPrimary
|
||||
onClick={this.onSave}
|
||||
text={t('restore.save', {
|
||||
defaultValue: 'Restore'
|
||||
})}
|
||||
/>
|
||||
</Button.Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderInput () {
|
||||
const { t } = this.props;
|
||||
const { isFileValid, isPassValid, password } = this.state;
|
||||
|
||||
return (
|
||||
<div className='grow'>
|
||||
<div className='ui--row'>
|
||||
<Password
|
||||
className='full'
|
||||
isError={!isPassValid}
|
||||
label={t('restore.password', {
|
||||
defaultValue: 'decrypt keyfile using the password'
|
||||
})}
|
||||
onChange={this.onChangePass}
|
||||
value={password}
|
||||
/>
|
||||
</div>
|
||||
<div className='ui--row'>
|
||||
<InputFile
|
||||
className='full'
|
||||
isError={!isFileValid}
|
||||
label={t('restore.json', {
|
||||
defaultValue: 'previously backed-up json keyfile'
|
||||
})}
|
||||
onChange={this.onChangeFile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onChangeFile = (file: Uint8Array): void => {
|
||||
try {
|
||||
const json = JSON.parse(u8aToUtf8(file));
|
||||
const isFileValid = decodeAddress(json.address).length === 32 &&
|
||||
isHex(json.encoded) &&
|
||||
isObject(json.meta) &&
|
||||
json.encoding.content === 'pkcs8';
|
||||
|
||||
this.setState({
|
||||
isFileValid,
|
||||
json
|
||||
});
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
isFileValid: false,
|
||||
json: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onChangePass = (password: string): void => {
|
||||
this.setState({
|
||||
isPassValid: password.length > 0 && password.length <= 32,
|
||||
password
|
||||
});
|
||||
}
|
||||
|
||||
onSave = (): void => {
|
||||
const { onBack } = this.props;
|
||||
const { json, password } = this.state;
|
||||
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pair = createPair(
|
||||
{
|
||||
publicKey: decodeAddress(json.address),
|
||||
secretKey: new Uint8Array()
|
||||
},
|
||||
json.meta,
|
||||
hexToU8a(json.encoded)
|
||||
);
|
||||
|
||||
pair.decodePkcs8(password);
|
||||
pair.lock();
|
||||
|
||||
keyring.addAccountPair(pair, password);
|
||||
InputAddress.setLastValue('account', pair.address());
|
||||
onBack();
|
||||
} catch (error) {
|
||||
this.setState({ isPassValid: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(Restore);
|
||||
@ -14,6 +14,7 @@ import withObservableBase from '@polkadot/ui-react-rx/with/observableBase';
|
||||
|
||||
import Creator from './Creator';
|
||||
import Editor from './Editor';
|
||||
import Restore from './Restore';
|
||||
import translate from './translate';
|
||||
|
||||
type Props = I18nProps & {
|
||||
@ -21,7 +22,7 @@ type Props = I18nProps & {
|
||||
basePath: string
|
||||
};
|
||||
|
||||
type Actions = 'create' | 'edit';
|
||||
type Actions = 'create' | 'edit' | 'restore';
|
||||
|
||||
type State = {
|
||||
action: Actions,
|
||||
@ -32,7 +33,8 @@ type State = {
|
||||
// FIXME React-router would probably be the best route, not home-grown
|
||||
const Components: { [index: string]: React.ComponentType<any> } = {
|
||||
'create': Creator,
|
||||
'edit': Editor
|
||||
'edit': Editor,
|
||||
'restore': Restore
|
||||
};
|
||||
|
||||
class AccountsApp extends React.PureComponent<Props, State> {
|
||||
@ -56,6 +58,10 @@ class AccountsApp extends React.PureComponent<Props, State> {
|
||||
{
|
||||
name: 'create',
|
||||
text: t('app.create', { defaultValue: 'Create account' })
|
||||
},
|
||||
{
|
||||
name: 'restore',
|
||||
text: t('app.restore', { defaultValue: 'Restore account' })
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@ -21,7 +21,7 @@ export type Props = I18nProps & {
|
||||
balance?: BN | Array<BN>,
|
||||
children?: React.ReactNode,
|
||||
name?: string,
|
||||
value: string | Uint8Array,
|
||||
value: string | Uint8Array | null,
|
||||
withBalance?: boolean,
|
||||
identIconSize?: number,
|
||||
isShort?: boolean
|
||||
@ -45,7 +45,7 @@ class AddressSummary extends React.PureComponent<Props, State> {
|
||||
|
||||
static getDerivedStateFromProps ({ value }: Props, { address, publicKey, shortValue }: State): State {
|
||||
try {
|
||||
publicKey = addressDecode(value);
|
||||
publicKey = addressDecode(value as string);
|
||||
address = addressEncode(publicKey);
|
||||
shortValue = toShortAddress(address);
|
||||
} catch (error) {
|
||||
@ -121,9 +121,9 @@ class AddressSummary extends React.PureComponent<Props, State> {
|
||||
|
||||
protected renderCopy () {
|
||||
const { withCopy = true } = this.props;
|
||||
const { address } = this.state;
|
||||
const { address, publicKey } = this.state;
|
||||
|
||||
if (!withCopy) {
|
||||
if (!withCopy || !publicKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
104
packages/ui-app/src/InputFile.tsx
Normal file
104
packages/ui-app/src/InputFile.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
// Copyright 2017-2018 @polkadot/ui-app authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { TranslationFunction } from 'i18next';
|
||||
import { BareProps } from '@polkadot/ui-app/types';
|
||||
|
||||
import React from 'react';
|
||||
import Dropzone from 'react-dropzone';
|
||||
|
||||
import Labelled from './Labelled';
|
||||
import classes from './util/classes';
|
||||
import translate from './translate';
|
||||
|
||||
type Props = BareProps & {
|
||||
isDisabled?: boolean,
|
||||
isError?: boolean,
|
||||
label: string,
|
||||
onChange?: (contents: Uint8Array) => void,
|
||||
placeholder?: string,
|
||||
t: TranslationFunction,
|
||||
withLabel?: boolean
|
||||
};
|
||||
|
||||
type State = {
|
||||
file?: {
|
||||
name: string,
|
||||
size: number
|
||||
}
|
||||
};
|
||||
|
||||
type LoadEvent = {
|
||||
target: {
|
||||
result: ArrayBuffer
|
||||
}
|
||||
};
|
||||
|
||||
class InputFile extends React.PureComponent<Props, State> {
|
||||
state: State = {};
|
||||
|
||||
render () {
|
||||
const { className, isDisabled, isError = false, label, placeholder, t, withLabel } = this.props;
|
||||
const { file } = this.state;
|
||||
|
||||
return (
|
||||
<Labelled
|
||||
label={label}
|
||||
withLabel={withLabel}
|
||||
>
|
||||
<Dropzone
|
||||
className={classes('ui--InputFile', isError ? 'error' : '', className)}
|
||||
disabled={isDisabled}
|
||||
multiple={false}
|
||||
onDrop={this.onDrop}
|
||||
>
|
||||
<div className='label'>
|
||||
{
|
||||
!file
|
||||
? placeholder || t('file.dnd', {
|
||||
defaultValue: 'drag and drop the file here'
|
||||
})
|
||||
: placeholder || t('file.description', {
|
||||
defaultValue: '{{name}} ({{size}} bytes)',
|
||||
replace: file
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</Dropzone>
|
||||
</Labelled>
|
||||
);
|
||||
}
|
||||
|
||||
onDrop = (files: Array<File>) => {
|
||||
const { onChange } = this.props;
|
||||
|
||||
files.forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onabort = () => {
|
||||
// ignore
|
||||
};
|
||||
reader.onerror = () => {
|
||||
// ignore
|
||||
};
|
||||
// @ts-ignore ummm... events are not properly specified here?
|
||||
reader.onload = ({ target: { result } }: LoadEvent) => {
|
||||
const data = new Uint8Array(result);
|
||||
|
||||
onChange && onChange(data);
|
||||
|
||||
this.setState({
|
||||
file: {
|
||||
name: file.name,
|
||||
size: data.length
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(InputFile);
|
||||
@ -2,15 +2,12 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { TranslationFunction } from 'i18next';
|
||||
import { BareProps } from '@polkadot/ui-app/types';
|
||||
|
||||
import React from 'react';
|
||||
import Dropzone from 'react-dropzone';
|
||||
|
||||
import classes from '../../util/classes';
|
||||
import translate from '../../translate';
|
||||
import Base from './Base';
|
||||
import InputFile from '../../InputFile';
|
||||
import Bare from './Bare';
|
||||
|
||||
type Props = BareProps & {
|
||||
isDisabled?: boolean,
|
||||
@ -18,88 +15,27 @@ type Props = BareProps & {
|
||||
label: string,
|
||||
onChange?: (contents: Uint8Array) => void,
|
||||
placeholder?: string,
|
||||
t: TranslationFunction,
|
||||
withLabel?: boolean
|
||||
};
|
||||
|
||||
type State = {
|
||||
file?: {
|
||||
name: string,
|
||||
size: number
|
||||
}
|
||||
};
|
||||
|
||||
type LoadEvent = {
|
||||
target: { // eslint-disable-line react/no-unused-prop-types
|
||||
result: ArrayBuffer
|
||||
}
|
||||
};
|
||||
|
||||
class BytesFile extends React.PureComponent<Props, State> {
|
||||
state: State = {};
|
||||
|
||||
export default class File extends React.PureComponent<Props> {
|
||||
render () {
|
||||
const { className, isDisabled, isError = false, label, placeholder, t, withLabel } = this.props;
|
||||
const { file } = this.state;
|
||||
const { className, isDisabled, isError = false, label, onChange, placeholder, style, withLabel } = this.props;
|
||||
|
||||
return (
|
||||
<Base
|
||||
label={label}
|
||||
size='full'
|
||||
withLabel={withLabel}
|
||||
<Bare
|
||||
className={className}
|
||||
style={style}
|
||||
>
|
||||
<Dropzone
|
||||
className={classes('ui--Param-File', isError ? 'error' : '', className)}
|
||||
disabled={isDisabled}
|
||||
multiple={false}
|
||||
onDrop={this.onDrop}
|
||||
>
|
||||
<div className='label'>
|
||||
{
|
||||
!file
|
||||
? placeholder || t('file.dnd', {
|
||||
defaultValue: 'drag and drop the file here'
|
||||
})
|
||||
: placeholder || t('file.description', {
|
||||
defaultValue: '{{name}} ({{size}} bytes)',
|
||||
replace: file
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</Dropzone>
|
||||
</Base>
|
||||
<InputFile
|
||||
isDisabled={isDisabled}
|
||||
isError={isError}
|
||||
label={label}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
withLabel={withLabel}
|
||||
/>
|
||||
</Bare>
|
||||
);
|
||||
}
|
||||
|
||||
onDrop = (files: Array<File>) => {
|
||||
const { onChange } = this.props;
|
||||
|
||||
files.forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onabort = () => {
|
||||
// ignore
|
||||
};
|
||||
reader.onerror = () => {
|
||||
// ignore
|
||||
};
|
||||
// @ts-ignore ummm... events are not properly specified here?
|
||||
reader.onload = ({ target: { result } }: LoadEvent) => {
|
||||
const data = new Uint8Array(result);
|
||||
|
||||
onChange && onChange(data);
|
||||
|
||||
this.setState({
|
||||
file: {
|
||||
name: file.name,
|
||||
size: data.length
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(BytesFile);
|
||||
|
||||
@ -11,31 +11,6 @@
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.ui--Param-File {
|
||||
align-items: center;
|
||||
border: 1px dashed rgba(34, 36, 38, 0.15);
|
||||
border-radius: 0.28571429rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 1.2em;
|
||||
height: 5em;
|
||||
margin-bottom: 0.5rem;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.ui--Param-File.true {
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.ui--Params-File:hover {
|
||||
background: rgba(200, 200, 200, 0.8);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui--Param-File .label {
|
||||
color: rgba(0, 0, 0, .6);
|
||||
}
|
||||
|
||||
.ui--Param-text {
|
||||
display: inline-block;
|
||||
font-size: 1rem;
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
/* This software may be modified and distributed under the terms
|
||||
/* of the ISC license. See the LICENSE file for details. */
|
||||
|
||||
@import './partials/_variables.css';
|
||||
|
||||
.ui--output {
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
@ -26,7 +28,7 @@
|
||||
}
|
||||
|
||||
.ui--Button-Group .button {
|
||||
border-radius: .28571429rem !important;
|
||||
border-radius: $small-corner !important;
|
||||
}
|
||||
|
||||
header .ui--Button-Group {
|
||||
@ -166,3 +168,29 @@ header .ui--Button-Group {
|
||||
.ui--CopyButton {
|
||||
cursor: copy !important;
|
||||
}
|
||||
|
||||
.ui--InputFile {
|
||||
align-items: center;
|
||||
border: 1px dashed rgba(34, 36, 38, 0.15);
|
||||
border-radius: $small-corner;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 1.2em;
|
||||
height: 5em;
|
||||
margin-bottom: 0.5rem;
|
||||
width: 100% !important;
|
||||
|
||||
&.error {
|
||||
background: #fff6f6;
|
||||
border-color: #e0b4b4;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #fefefe;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: rgba(0, 0, 0, .6);
|
||||
}
|
||||
}
|
||||
|
||||
16
packages/ui-keyring/src/account/addPair.ts
Normal file
16
packages/ui-keyring/src/account/addPair.ts
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright 2017-2018 @polkadot/ui-keyring authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { KeyringPair } from '@polkadot/util-keyring/types';
|
||||
import { State } from '../types';
|
||||
|
||||
import saveAccount from './save';
|
||||
|
||||
export default function addAccountPair (state: State, pair: KeyringPair, password: string): KeyringPair {
|
||||
state.keyring.addPair(pair);
|
||||
|
||||
saveAccount(state, pair, password);
|
||||
|
||||
return pair;
|
||||
}
|
||||
@ -12,6 +12,7 @@ import accounts from './observable/accounts';
|
||||
import addresses from './observable/addresses';
|
||||
import development from './observable/development';
|
||||
import loadAll from './loadAll';
|
||||
import addAccountPair from './account/addPair';
|
||||
import createAccount from './account/create';
|
||||
import forgetAccount from './account/forget';
|
||||
import isAvailable from './isAvailable';
|
||||
@ -33,7 +34,11 @@ const state: State = {
|
||||
|
||||
loadAll(state);
|
||||
|
||||
// FIXME The quicker we get in https://github.com/polkadot-js/apps/issues/138
|
||||
// the better, this is now completely out of control
|
||||
export default ({
|
||||
addAccountPair: (pair: KeyringPair, password: string): KeyringPair =>
|
||||
addAccountPair(state, pair, password),
|
||||
createAccount: (seed: Uint8Array, password?: string, meta?: KeyringPair$Meta): KeyringPair =>
|
||||
createAccount(state, seed, password, meta),
|
||||
encryptAccount: (pair: KeyringPair, password: string): void =>
|
||||
|
||||
1
packages/ui-keyring/src/types.d.ts
vendored
1
packages/ui-keyring/src/types.d.ts
vendored
@ -35,6 +35,7 @@ export type State = {
|
||||
};
|
||||
|
||||
export type KeyringInstance = {
|
||||
addAccountPair: (pair: KeyringPair, password: string) => KeyringPair,
|
||||
createAccount: (seed: Uint8Array, password?: string, meta?: KeyringPair$Meta) => KeyringPair,
|
||||
encryptAccount: (pair: KeyringPair, password: string) => void,
|
||||
forgetAccount: (address: string) => void,
|
||||
|
||||
@ -1630,6 +1630,10 @@
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
|
||||
|
||||
"@types/file-saver@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-1.3.0.tgz#0ef213077e704fc3f4e7a86cfd31c9de4f4f47a7"
|
||||
|
||||
"@types/fs-extra@^5.0.3":
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.0.4.tgz#b971134d162cc0497d221adde3dbb67502225599"
|
||||
@ -4869,6 +4873,10 @@ file-loader@^2.0.0:
|
||||
loader-utils "^1.0.2"
|
||||
schema-utils "^1.0.0"
|
||||
|
||||
file-saver@^1.3.8:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8"
|
||||
|
||||
filename-regex@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user