refactor restoration (#554)
Co-authored-by: Github Actions <action@github.com> Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
co-authored by
Github Actions
Jaco Greeff
parent
b56f869118
commit
3b4c01ac5d
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user