refactor restoration (#554)
Co-authored-by: Github Actions <action@github.com> Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
parent
b56f869118
commit
3b4c01ac5d
@ -2,16 +2,16 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import type { MetadataDef } from '@polkadot/extension-inject/types';
|
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 { 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 { ALLOWED_PATH, PASSWORD_EXPIRY_MS } from '@polkadot/extension-base/defaults';
|
||||||
import chrome from '@polkadot/extension-inject/chrome';
|
import chrome from '@polkadot/extension-inject/chrome';
|
||||||
import keyring from '@polkadot/ui-keyring';
|
import keyring from '@polkadot/ui-keyring';
|
||||||
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
|
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
|
||||||
import { TypeRegistry } from '@polkadot/types';
|
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 { keyExtractSuri, mnemonicGenerate, mnemonicValidate } from '@polkadot/util-crypto';
|
||||||
|
|
||||||
import State from './State';
|
import State from './State';
|
||||||
@ -242,39 +242,27 @@ export default class Extension {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private jsonRestore ({ json, password }: RequestJsonRestore): ResponseJsonRestore {
|
private jsonRestore ({ file, password }: RequestJsonRestore): void {
|
||||||
try {
|
try {
|
||||||
const pair = keyring.restoreAccount(json, password);
|
keyring.restoreAccount(file, password);
|
||||||
|
|
||||||
if (pair) {
|
|
||||||
return { error: null };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} 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 {
|
try {
|
||||||
const publicKey = keyring.decodeAddress(json.address, true);
|
const pair = keyring.createFromJson(json);
|
||||||
const isFileValid = publicKey.length === 32 && !!json.encoded && isObject(json.meta) && (
|
|
||||||
Array.isArray(json.encoding.content)
|
|
||||||
? json.encoding.content[0] === 'pkcs8'
|
|
||||||
: json.encoding.content === 'pkcs8'
|
|
||||||
);
|
|
||||||
|
|
||||||
return isFileValid;
|
return {
|
||||||
} catch (error) {
|
address: pair.address,
|
||||||
console.error(error);
|
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 {
|
private seedCreate ({ length = SEED_DEFAULT_LENGTH, type }: RequestSeedCreate): ResponseSeedCreate {
|
||||||
@ -526,11 +514,8 @@ export default class Extension {
|
|||||||
case 'pri(json.restore)':
|
case 'pri(json.restore)':
|
||||||
return this.jsonRestore(request as RequestJsonRestore);
|
return this.jsonRestore(request as RequestJsonRestore);
|
||||||
|
|
||||||
case 'pri(json.verify.file)':
|
case 'pri(json.account.info)':
|
||||||
return this.jsonVerifyFile(request as RequestJsonRestore);
|
return this.jsonGetAccountInfo(request as KeyringPair$Json);
|
||||||
|
|
||||||
case 'pri(json.verify.password)':
|
|
||||||
return this.jsonVerifyPassword(request as string);
|
|
||||||
|
|
||||||
case 'pri(seed.create)':
|
case 'pri(seed.create)':
|
||||||
return this.seedCreate(request as RequestSeedCreate);
|
return this.seedCreate(request as RequestSeedCreate);
|
||||||
|
|||||||
@ -85,9 +85,8 @@ export interface RequestSignatures {
|
|||||||
'pri(authorize.requests)': [RequestAuthorizeSubscribe, boolean, AuthorizeRequest[]];
|
'pri(authorize.requests)': [RequestAuthorizeSubscribe, boolean, AuthorizeRequest[]];
|
||||||
'pri(derivation.create)': [RequestDeriveCreate, boolean];
|
'pri(derivation.create)': [RequestDeriveCreate, boolean];
|
||||||
'pri(derivation.validate)': [RequestDeriveValidate, ResponseDeriveValidate];
|
'pri(derivation.validate)': [RequestDeriveValidate, ResponseDeriveValidate];
|
||||||
'pri(json.restore)': [RequestJsonRestore, ResponseJsonRestore];
|
'pri(json.restore)': [RequestJsonRestore, void];
|
||||||
'pri(json.verify.file)': [RequestJsonRestore, boolean];
|
'pri(json.account.info)': [KeyringPair$Json, ResponseJsonGetAccountInfo];
|
||||||
'pri(json.verify.password)': [string, boolean];
|
|
||||||
'pri(metadata.approve)': [RequestMetadataApprove, boolean];
|
'pri(metadata.approve)': [RequestMetadataApprove, boolean];
|
||||||
'pri(metadata.get)': [string | null, MetadataDef | null];
|
'pri(metadata.get)': [string | null, MetadataDef | null];
|
||||||
'pri(metadata.reject)': [RequestMetadataReject, boolean];
|
'pri(metadata.reject)': [RequestMetadataReject, boolean];
|
||||||
@ -349,7 +348,7 @@ export interface RequestSign {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestJsonRestore {
|
export interface RequestJsonRestore {
|
||||||
json: KeyringPair$Json;
|
file: KeyringPair$Json;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,3 +357,9 @@ export interface ResponseJsonRestore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AllowedPath = typeof ALLOWED_PATH[number];
|
export type AllowedPath = typeof ALLOWED_PATH[number];
|
||||||
|
|
||||||
|
export interface ResponseJsonGetAccountInfo {
|
||||||
|
address: string;
|
||||||
|
name: string;
|
||||||
|
genesisHash: string;
|
||||||
|
}
|
||||||
|
|||||||
@ -7,120 +7,129 @@ import React, { useCallback, useContext, useEffect, useState } from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { u8aToString } from '@polkadot/util';
|
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 useTranslation from '../hooks/useTranslation';
|
||||||
import { jsonRestore, jsonVerifyPassword, jsonVerifyFile } from '../messaging';
|
import { jsonRestore, jsonGetAccountInfo } from '../messaging';
|
||||||
import { Header } from '../partials';
|
import { Header } from '../partials';
|
||||||
|
import { ResponseJsonGetAccountInfo } from '@polkadot/extension-base/background/types';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const acceptedFormats = ['application/json', 'text/plain'].join(', ');
|
const acceptedFormats = ['application/json', 'text/plain'].join(', ');
|
||||||
|
|
||||||
async function parseFile (file: Uint8Array): Promise<FileState> {
|
interface Props {
|
||||||
try {
|
className?: string;
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Upload (): React.ReactElement {
|
function Upload ({ className } : Props): React.ReactElement {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { accounts } = useContext(AccountContext);
|
const { accounts } = useContext(AccountContext);
|
||||||
const onAction = useContext(ActionContext);
|
const onAction = useContext(ActionContext);
|
||||||
const [isBusy, setIsBusy] = useState(false);
|
const [isBusy, setIsBusy] = useState(false);
|
||||||
const [{ address, isFileValid, json }, setJson] = useState<FileState>({ address: null, isFileValid: false, json: null });
|
const [{ address, genesisHash, name }, setAccountInfo] = useState<ResponseJsonGetAccountInfo>({ address: '', genesisHash: '', name: '' });
|
||||||
const [{ isPassValid, password }, setPass] = useState<PassState>({ isPassValid: false, password: '' });
|
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 => {
|
useEffect((): void => {
|
||||||
!accounts.length && onAction();
|
!accounts.length && onAction();
|
||||||
}, [accounts, onAction]);
|
}, [accounts, onAction]);
|
||||||
|
|
||||||
const _onChangePass = useCallback(
|
const _onChangePass = useCallback(
|
||||||
(password: string): void => {
|
(pass: string): void => {
|
||||||
jsonVerifyPassword(password)
|
setPassword(pass);
|
||||||
.then((isPassValid) => setPass({ isPassValid, password }))
|
setIsPasswordError(false);
|
||||||
.catch(console.error);
|
|
||||||
}, []
|
}, []
|
||||||
);
|
);
|
||||||
|
|
||||||
const _onChangeFile = useCallback(
|
const _onChangeFile = useCallback(
|
||||||
(file: Uint8Array): void => {
|
(file: Uint8Array): void => {
|
||||||
parseFile(file)
|
let json: KeyringPair$Json | undefined;
|
||||||
.then(setJson)
|
|
||||||
.catch(console.error);
|
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(
|
const _onRestore = useCallback(
|
||||||
(): void => {
|
(): void => {
|
||||||
if (!json || !password) {
|
if (!file || !password) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsBusy(true);
|
setIsBusy(true);
|
||||||
|
|
||||||
jsonRestore(json, password)
|
jsonRestore(file, password)
|
||||||
.then(({ error }): void => {
|
.then(() => { onAction('/'); })
|
||||||
if (error) {
|
.catch((e) => {
|
||||||
setIsBusy(false);
|
console.error(e);
|
||||||
setPass(({ password }) => ({ decodeError: error, isPassValid: false, password }));
|
setIsBusy(false);
|
||||||
} else {
|
setIsPasswordError(true);
|
||||||
onAction('/');
|
});
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
},
|
},
|
||||||
[json, onAction, password]
|
[file, onAction, password]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeaderWithSmallerMargin
|
<Header
|
||||||
showBackArrow
|
showBackArrow
|
||||||
|
smallMargin
|
||||||
text={t<string>('Restore from JSON')}
|
text={t<string>('Restore from JSON')}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div className={className}>
|
||||||
<div>
|
<div>
|
||||||
<Address
|
<Address
|
||||||
address={(isFileValid && address) || null}
|
address={address}
|
||||||
genesisHash={(isFileValid && json?.meta.genesisHash as string) || null}
|
genesisHash={genesisHash}
|
||||||
name={(isFileValid && json?.meta.name as string) || null}
|
name={name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<InputFileWithLabel
|
<InputFileWithLabel
|
||||||
accept={acceptedFormats}
|
accept={acceptedFormats}
|
||||||
isError={!isFileValid}
|
isError={isFileError}
|
||||||
label={t<string>('backup file')}
|
label={t<string>('backup file')}
|
||||||
onChange={_onChangeFile}
|
onChange={_onChangeFile}
|
||||||
withLabel
|
withLabel
|
||||||
/>
|
/>
|
||||||
|
{isFileError && (
|
||||||
|
<Warning
|
||||||
|
isDanger
|
||||||
|
>
|
||||||
|
{t<string>('Invalid Json file')}
|
||||||
|
</Warning>
|
||||||
|
)}
|
||||||
<InputWithLabel
|
<InputWithLabel
|
||||||
isError={!isPassValid}
|
isError={isPasswordError}
|
||||||
label={t<string>('Password for this file')}
|
label={t<string>('Password for this file')}
|
||||||
onChange={_onChangePass}
|
onChange={_onChangePass}
|
||||||
type='password'
|
type='password'
|
||||||
/>
|
/>
|
||||||
|
{isPasswordError && (
|
||||||
|
<Warning
|
||||||
|
isBelowInput
|
||||||
|
isDanger
|
||||||
|
>
|
||||||
|
{t<string>('Unable to decode using the supplied passphrase')}
|
||||||
|
</Warning>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
className='restoreButton'
|
||||||
isBusy={isBusy}
|
isBusy={isBusy}
|
||||||
isDisabled={!isFileValid || !isPassValid}
|
isDisabled={isFileError || isPasswordError}
|
||||||
onClick={_onRestore}
|
onClick={_onRestore}
|
||||||
>
|
>
|
||||||
{t<string>('Restore')}
|
{t<string>('Restore')}
|
||||||
@ -130,6 +139,8 @@ export default function Upload (): React.ReactElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderWithSmallerMargin = styled(Header)`
|
export default styled(Upload)`
|
||||||
margin-bottom: 15px;
|
.restoreButton {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import type { Message } from '@polkadot/extension-base/types';
|
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 { Chain } from '@polkadot/extension-chains/types';
|
||||||
import type { MetadataDef } from '@polkadot/extension-inject/types';
|
import type { MetadataDef } from '@polkadot/extension-inject/types';
|
||||||
import type { KeyringPair$Json } from '@polkadot/keyring/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);
|
return sendMessage('pri(window.open)', path);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function jsonVerifyFile (json: KeyringPair$Json): Promise<boolean> {
|
export async function jsonGetAccountInfo (json: KeyringPair$Json): Promise<ResponseJsonGetAccountInfo> {
|
||||||
return sendMessage('pri(json.verify.file)', { json, password: '' });
|
return sendMessage('pri(json.account.info)', json);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function jsonVerifyPassword (password: string): Promise<boolean> {
|
export async function jsonRestore (file: KeyringPair$Json, password: string): Promise<void> {
|
||||||
return sendMessage('pri(json.verify.password)', password);
|
return sendMessage('pri(json.restore)', { file, password });
|
||||||
}
|
|
||||||
|
|
||||||
export async function jsonRestore (json: KeyringPair$Json, password: string): Promise<ResponseJsonRestore> {
|
|
||||||
return sendMessage('pri(json.restore)', { json, password });
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user