refactor restoration (#554)

Co-authored-by: Github Actions <action@github.com>
Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
Thibaut Sardan
2020-12-03 15:25:22 +01:00
committed by GitHub
co-authored by Github Actions Jaco Greeff
parent b56f869118
commit 3b4c01ac5d
4 changed files with 102 additions and 105 deletions
@@ -2,16 +2,16 @@
// SPDX-License-Identifier: Apache-2.0
import type { MetadataDef } from '@polkadot/extension-inject/types';
import type { KeyringPair, KeyringPair$Meta } from '@polkadot/keyring/types';
import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@polkadot/keyring/types';
import type { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestDeriveCreate, ResponseDeriveValidate, RequestMetadataApprove, RequestMetadataReject, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestSeedCreate, RequestTypes, ResponseAccountExport, RequestAccountForget, ResponseSeedCreate, RequestSeedValidate, RequestDeriveValidate, RequestJsonRestore, ResponseJsonRestore, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types';
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestDeriveCreate, ResponseDeriveValidate, RequestMetadataApprove, RequestMetadataReject, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestSeedCreate, RequestTypes, ResponseAccountExport, RequestAccountForget, ResponseSeedCreate, RequestSeedValidate, RequestDeriveValidate, RequestJsonRestore, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest, ResponseJsonGetAccountInfo } from '../types';
import { ALLOWED_PATH, PASSWORD_EXPIRY_MS } from '@polkadot/extension-base/defaults';
import chrome from '@polkadot/extension-inject/chrome';
import keyring from '@polkadot/ui-keyring';
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
import { TypeRegistry } from '@polkadot/types';
import { assert, isHex, isObject } from '@polkadot/util';
import { assert, isHex } from '@polkadot/util';
import { keyExtractSuri, mnemonicGenerate, mnemonicValidate } from '@polkadot/util-crypto';
import State from './State';
@@ -242,39 +242,27 @@ export default class Extension {
return true;
}
private jsonRestore ({ json, password }: RequestJsonRestore): ResponseJsonRestore {
private jsonRestore ({ file, password }: RequestJsonRestore): void {
try {
const pair = keyring.restoreAccount(json, password);
if (pair) {
return { error: null };
}
keyring.restoreAccount(file, password);
} catch (error) {
return { error: (error as Error).message };
throw new Error((error as Error).message);
}
return { error: 'Could not restore account.' };
}
private jsonVerifyFile ({ json }: RequestJsonRestore): boolean {
private jsonGetAccountInfo (json: KeyringPair$Json): ResponseJsonGetAccountInfo {
try {
const publicKey = keyring.decodeAddress(json.address, true);
const isFileValid = publicKey.length === 32 && !!json.encoded && isObject(json.meta) && (
Array.isArray(json.encoding.content)
? json.encoding.content[0] === 'pkcs8'
: json.encoding.content === 'pkcs8'
);
const pair = keyring.createFromJson(json);
return isFileValid;
} catch (error) {
console.error(error);
return {
address: pair.address,
genesisHash: pair.meta.genesisHash,
name: pair.meta.name
} as ResponseJsonGetAccountInfo;
} catch (e) {
console.error(e);
throw new Error((e as Error).message);
}
return false;
}
private jsonVerifyPassword (password: string): boolean {
return keyring.isPassValid(password);
}
private seedCreate ({ length = SEED_DEFAULT_LENGTH, type }: RequestSeedCreate): ResponseSeedCreate {
@@ -526,11 +514,8 @@ export default class Extension {
case 'pri(json.restore)':
return this.jsonRestore(request as RequestJsonRestore);
case 'pri(json.verify.file)':
return this.jsonVerifyFile(request as RequestJsonRestore);
case 'pri(json.verify.password)':
return this.jsonVerifyPassword(request as string);
case 'pri(json.account.info)':
return this.jsonGetAccountInfo(request as KeyringPair$Json);
case 'pri(seed.create)':
return this.seedCreate(request as RequestSeedCreate);
@@ -85,9 +85,8 @@ export interface RequestSignatures {
'pri(authorize.requests)': [RequestAuthorizeSubscribe, boolean, AuthorizeRequest[]];
'pri(derivation.create)': [RequestDeriveCreate, boolean];
'pri(derivation.validate)': [RequestDeriveValidate, ResponseDeriveValidate];
'pri(json.restore)': [RequestJsonRestore, ResponseJsonRestore];
'pri(json.verify.file)': [RequestJsonRestore, boolean];
'pri(json.verify.password)': [string, boolean];
'pri(json.restore)': [RequestJsonRestore, void];
'pri(json.account.info)': [KeyringPair$Json, ResponseJsonGetAccountInfo];
'pri(metadata.approve)': [RequestMetadataApprove, boolean];
'pri(metadata.get)': [string | null, MetadataDef | null];
'pri(metadata.reject)': [RequestMetadataReject, boolean];
@@ -349,7 +348,7 @@ export interface RequestSign {
}
export interface RequestJsonRestore {
json: KeyringPair$Json;
file: KeyringPair$Json;
password: string;
}
@@ -358,3 +357,9 @@ export interface ResponseJsonRestore {
}
export type AllowedPath = typeof ALLOWED_PATH[number];
export interface ResponseJsonGetAccountInfo {
address: string;
name: string;
genesisHash: string;
}
+70 -59
View File
@@ -7,120 +7,129 @@ import React, { useCallback, useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import { u8aToString } from '@polkadot/util';
import { AccountContext, ActionContext, InputWithLabel, InputFileWithLabel, Button, Address } from '../components';
import { AccountContext, ActionContext, InputWithLabel, InputFileWithLabel, Button, Address, Warning } from '../components';
import useTranslation from '../hooks/useTranslation';
import { jsonRestore, jsonVerifyPassword, jsonVerifyFile } from '../messaging';
import { jsonRestore, jsonGetAccountInfo } from '../messaging';
import { Header } from '../partials';
interface FileState {
address: string | null;
isFileValid: boolean;
json: KeyringPair$Json | null;
}
// FIXME We want to display the decodeError
interface PassState {
decodeError?: string | null;
isPassValid: boolean;
password: string;
}
import { ResponseJsonGetAccountInfo } from '@polkadot/extension-base/background/types';
const acceptedFormats = ['application/json', 'text/plain'].join(', ');
async function parseFile (file: Uint8Array): Promise<FileState> {
try {
const json = JSON.parse(u8aToString(file)) as KeyringPair$Json;
const isFileValid = await jsonVerifyFile(json);
const address = json.address;
return { address, isFileValid, json };
} catch (error) {
console.error(error);
}
return { address: null, isFileValid: false, json: null };
interface Props {
className?: string;
}
export default function Upload (): React.ReactElement {
function Upload ({ className } : Props): React.ReactElement {
const { t } = useTranslation();
const { accounts } = useContext(AccountContext);
const onAction = useContext(ActionContext);
const [isBusy, setIsBusy] = useState(false);
const [{ address, isFileValid, json }, setJson] = useState<FileState>({ address: null, isFileValid: false, json: null });
const [{ isPassValid, password }, setPass] = useState<PassState>({ isPassValid: false, password: '' });
const [{ address, genesisHash, name }, setAccountInfo] = useState<ResponseJsonGetAccountInfo>({ address: '', genesisHash: '', name: '' });
const [password, setPassword] = useState<string>('');
const [isFileError, setFileError] = useState(false);
const [isPasswordError, setIsPasswordError] = useState(false);
// don't use the info from the file directly
// rather use what comes from the background from jsonGetAccountInfo
const [file, setFile] = useState<KeyringPair$Json|undefined>(undefined);
useEffect((): void => {
!accounts.length && onAction();
}, [accounts, onAction]);
const _onChangePass = useCallback(
(password: string): void => {
jsonVerifyPassword(password)
.then((isPassValid) => setPass({ isPassValid, password }))
.catch(console.error);
(pass: string): void => {
setPassword(pass);
setIsPasswordError(false);
}, []
);
const _onChangeFile = useCallback(
(file: Uint8Array): void => {
parseFile(file)
.then(setJson)
.catch(console.error);
let json: KeyringPair$Json | undefined;
try {
json = JSON.parse(u8aToString(file)) as KeyringPair$Json;
setFile(json);
} catch (e) {
console.error(e);
setFileError(true);
}
json && jsonGetAccountInfo(json)
.then((accountInfo : ResponseJsonGetAccountInfo) => setAccountInfo(accountInfo))
.catch((e) => {
setFileError(true);
console.error(e);
});
}, []
);
const _onRestore = useCallback(
(): void => {
if (!json || !password) {
if (!file || !password) {
return;
}
setIsBusy(true);
jsonRestore(json, password)
.then(({ error }): void => {
if (error) {
setIsBusy(false);
setPass(({ password }) => ({ decodeError: error, isPassValid: false, password }));
} else {
onAction('/');
}
})
.catch(console.error);
jsonRestore(file, password)
.then(() => { onAction('/'); })
.catch((e) => {
console.error(e);
setIsBusy(false);
setIsPasswordError(true);
});
},
[json, onAction, password]
[file, onAction, password]
);
return (
<>
<HeaderWithSmallerMargin
<Header
showBackArrow
smallMargin
text={t<string>('Restore from JSON')}
/>
<div>
<div className={className}>
<div>
<Address
address={(isFileValid && address) || null}
genesisHash={(isFileValid && json?.meta.genesisHash as string) || null}
name={(isFileValid && json?.meta.name as string) || null}
address={address}
genesisHash={genesisHash}
name={name}
/>
</div>
<InputFileWithLabel
accept={acceptedFormats}
isError={!isFileValid}
isError={isFileError}
label={t<string>('backup file')}
onChange={_onChangeFile}
withLabel
/>
{isFileError && (
<Warning
isDanger
>
{t<string>('Invalid Json file')}
</Warning>
)}
<InputWithLabel
isError={!isPassValid}
isError={isPasswordError}
label={t<string>('Password for this file')}
onChange={_onChangePass}
type='password'
/>
{isPasswordError && (
<Warning
isBelowInput
isDanger
>
{t<string>('Unable to decode using the supplied passphrase')}
</Warning>
)}
<Button
className='restoreButton'
isBusy={isBusy}
isDisabled={!isFileValid || !isPassValid}
isDisabled={isFileError || isPasswordError}
onClick={_onRestore}
>
{t<string>('Restore')}
@@ -130,6 +139,8 @@ export default function Upload (): React.ReactElement {
);
}
const HeaderWithSmallerMargin = styled(Header)`
margin-bottom: 15px;
export default styled(Upload)`
.restoreButton {
margin-top: 6px;
}
`;
+5 -9
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { Message } from '@polkadot/extension-base/types';
import type { AccountJson, AllowedPath, AuthorizeRequest, SigningRequest, RequestTypes, MessageTypes, ResponseTypes, SeedLengths, SubscriptionMessageTypes, MetadataRequest, MessageTypesWithNullRequest, MessageTypesWithNoSubscriptions, MessageTypesWithSubscriptions, ResponseDeriveValidate, ResponseJsonRestore, ResponseSigningIsLocked } from '@polkadot/extension-base/background/types';
import type { AccountJson, AllowedPath, AuthorizeRequest, SigningRequest, RequestTypes, MessageTypes, ResponseTypes, SeedLengths, SubscriptionMessageTypes, MetadataRequest, MessageTypesWithNullRequest, MessageTypesWithNoSubscriptions, MessageTypesWithSubscriptions, ResponseDeriveValidate, ResponseSigningIsLocked, ResponseJsonGetAccountInfo } from '@polkadot/extension-base/background/types';
import type { Chain } from '@polkadot/extension-chains/types';
import type { MetadataDef } from '@polkadot/extension-inject/types';
import type { KeyringPair$Json } from '@polkadot/keyring/types';
@@ -198,14 +198,10 @@ export async function windowOpen (path: AllowedPath): Promise<boolean> {
return sendMessage('pri(window.open)', path);
}
export async function jsonVerifyFile (json: KeyringPair$Json): Promise<boolean> {
return sendMessage('pri(json.verify.file)', { json, password: '' });
export async function jsonGetAccountInfo (json: KeyringPair$Json): Promise<ResponseJsonGetAccountInfo> {
return sendMessage('pri(json.account.info)', json);
}
export async function jsonVerifyPassword (password: string): Promise<boolean> {
return sendMessage('pri(json.verify.password)', password);
}
export async function jsonRestore (json: KeyringPair$Json, password: string): Promise<ResponseJsonRestore> {
return sendMessage('pri(json.restore)', { json, password });
export async function jsonRestore (file: KeyringPair$Json, password: string): Promise<void> {
return sendMessage('pri(json.restore)', { file, password });
}