Allow account creation by derivation from existing one (#296)
* Allow specifying derivation path on account creation (#57) * Allow specifying derivation path on account creation * Improve linting and formatting * Highlight input field when der-path is invalid * Fix header * Hide advanced options * Add tests * Fix name * Improve styling * Change names * Fix case * Add arrow icon * Add smooth transition * Refactor tests Co-authored-by: Bartłomiej Rutkowski <bar.rutkow@gmail.com> * [CI Skip] 0.22.0-beta.7 skip-checks: true * Import account derivation path (#59) * Import account derivation path * Fix test * fix scroll * Lint fix * add tests to importSeed Co-authored-by: Bartłomiej Rutkowski <bartek@ethworks.io> Co-authored-by: Kamil <kamil@ethworks.io> * [CI Skip] 0.22.0-beta.8 skip-checks: true * Hover and copy button (#58) * copy button and change hover * working on * add Toast * chage address width * Minor fixes * Add cursor: hand * Finish merge Co-authored-by: Kamil <kamil@ethworks.io> Co-authored-by: ivan <ivanruch@gmail.com> * [CI Skip] 0.22.0-beta.9 skip-checks: true * Fix build * [CI Skip] 0.23.0-beta.0 skip-checks: true * [CI Skip] Undo Github Actions version bump * [CI Skip] 0.23.0-beta.5 skip-checks: true * Add derive screen (#60) * Add derivation screen * Remove "///password" from derivation path placeholder * Refactor tests Co-authored-by: Bartłomiej Rutkowski <bar.rutkow@gmail.com> Co-authored-by: Anze1m <bartek@ethworks.io> * Improve account menu; fix linting (#61) * Fix test * Fix account menu * Fix linting * [CI Skip] 0.23.0-beta.6 skip-checks: true * Add proper account decoding (#62) * Add proper account decoding * Improve tests and error handling * Fix linting * Add refactoring * Update packages/extension/src/background/handlers/Extension.ts Co-Authored-By: Ivan Rukhavets <ivanruch@gmail.com> * Fix test * Fix linting * Fix actions * Slight UX improvement * Add big red button * Change red button to orange * Wrap in useCallback * Fix linting * Add proper password checking method * Fix linting Co-authored-by: Ivan Rukhavets <ivanruch@gmail.com> * [CI Skip] 0.23.0-beta.7 skip-checks: true * Apply merge fixes (#65) * Fix linting * CI * [CI Skip] 0.24.0-beta.1 skip-checks: true * CR requests * [CI Skip] 0.24.0-beta.2 skip-checks: true * [CI Skip] 0.24.0-beta.2 skip-checks: true Co-authored-by: Ivan Rukhavets <ivanruch@gmail.com> Co-authored-by: Github Actions <action@github.com> Co-authored-by: Kamil <kamil@ethworks.io>
This commit is contained in:
parent
6d83833545
commit
eeac2c1064
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,3 +22,4 @@ yarn-error.log*
|
||||
package-lock.json
|
||||
_book
|
||||
docs/html
|
||||
.idea
|
||||
@ -9,5 +9,5 @@
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "0.24.0-beta.1"
|
||||
"version": "0.24.0-beta.2"
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@polkadot/extension-base",
|
||||
"description": "Functions, classes and other utilities used in @polkadot/extension",
|
||||
"version": "0.24.0-beta.1",
|
||||
"version": "0.24.0-beta.2",
|
||||
"author": "Jaco Greeff <jacogr@gmail.com>",
|
||||
"license": "Apache-2",
|
||||
"dependencies": {
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { ResponseAccountExport } from '../types';
|
||||
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
import ExtensionStore from '@polkadot/ui-keyring/stores/Extension';
|
||||
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
||||
@ -17,18 +15,92 @@ describe('Extension', () => {
|
||||
keyring.loadAll({ store: new ExtensionStore() });
|
||||
return new Extension(new State());
|
||||
}
|
||||
let extension: Extension;
|
||||
|
||||
beforeAll(async () => {
|
||||
extension = await createExtension();
|
||||
});
|
||||
|
||||
const suri = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
|
||||
const password = 'passw0rd';
|
||||
|
||||
test('exports account from keyring', async () => {
|
||||
const extension = await createExtension();
|
||||
const { pair: { address } } = keyring.addUri(
|
||||
'seed sock milk update focus rotate barely fade car face mechanic mercy', 'passw0rd'
|
||||
);
|
||||
const { pair: { address } } = keyring.addUri(suri, password);
|
||||
const result = await extension.handle('id', 'pri(accounts.export)', {
|
||||
address,
|
||||
password: 'passw0rd'
|
||||
}, {} as chrome.runtime.Port) as ResponseAccountExport;
|
||||
password
|
||||
}, {} as chrome.runtime.Port);
|
||||
|
||||
expect(result.exportedJson).toContain(address);
|
||||
expect(result.exportedJson).toContain('"encoded"');
|
||||
});
|
||||
|
||||
describe('account derivation', () => {
|
||||
const createAccount = async (): Promise<string> => {
|
||||
await extension.handle('id', 'pri(accounts.create.suri)', {
|
||||
name: 'parent',
|
||||
password,
|
||||
suri
|
||||
}, {} as chrome.runtime.Port);
|
||||
const { address } = await extension.handle('id', 'pri(seed.validate)', {
|
||||
suri
|
||||
}, {} as chrome.runtime.Port);
|
||||
return address;
|
||||
};
|
||||
let address: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
address = await createAccount();
|
||||
});
|
||||
|
||||
test('pri(derivation.validate) passes for valid suri', async () => {
|
||||
const result = await extension.handle('id', 'pri(derivation.validate)', {
|
||||
parentAddress: address,
|
||||
suri: '//path',
|
||||
parentPassword: password
|
||||
}, {} as chrome.runtime.Port);
|
||||
expect(result).toStrictEqual({
|
||||
address: '5FP3TT3EruYBNh8YM8yoxsreMx7uZv1J1zNX7fFhoC5enwmN',
|
||||
suri: '//path'
|
||||
});
|
||||
});
|
||||
|
||||
test('pri(derivation.validate) throws for invalid suri', async () => {
|
||||
await expect(extension.handle('id', 'pri(derivation.validate)', {
|
||||
parentAddress: address,
|
||||
suri: 'invalid-path',
|
||||
parentPassword: password
|
||||
}, {} as chrome.runtime.Port)).rejects.toStrictEqual(new Error('"invalid-path" is not a valid derivation path'));
|
||||
});
|
||||
|
||||
test('pri(derivation.validate) throws for invalid password', async () => {
|
||||
await expect(extension.handle('id', 'pri(derivation.validate)', {
|
||||
parentAddress: address,
|
||||
suri: '//path',
|
||||
parentPassword: 'invalid-password'
|
||||
}, {} as chrome.runtime.Port)).rejects.toStrictEqual(new Error('invalid password'));
|
||||
});
|
||||
|
||||
test('pri(derivation.create) adds a derived account', async () => {
|
||||
await extension.handle('id', 'pri(derivation.create)', {
|
||||
parentAddress: address,
|
||||
name: 'child',
|
||||
suri: '//path',
|
||||
parentPassword: password,
|
||||
password
|
||||
}, {} as chrome.runtime.Port);
|
||||
expect(keyring.getAccounts()).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('pri(derivation.create) saves parent address in meta', async () => {
|
||||
await extension.handle('id', 'pri(derivation.create)', {
|
||||
parentAddress: address,
|
||||
name: 'child',
|
||||
suri: '//path',
|
||||
parentPassword: password,
|
||||
password
|
||||
}, {} as chrome.runtime.Port);
|
||||
expect(keyring.getAccount('5FP3TT3EruYBNh8YM8yoxsreMx7uZv1J1zNX7fFhoC5enwmN')?.meta.parentAddress).toEqual(address);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -4,12 +4,42 @@
|
||||
|
||||
import { MetadataDef } from '@polkadot/extension-inject/types';
|
||||
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
|
||||
import { AccountJson, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAuthorizeApprove, RequestAuthorizeReject, RequestMetadataApprove, RequestMetadataReject, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSeedCreate, RequestTypes, ResponseAccountExport, RequestAccountForget, ResponseSeedCreate, RequestSeedValidate, ResponseSeedValidate, ResponseTypes, SigningRequest } from '../types';
|
||||
import {
|
||||
AccountJson,
|
||||
AuthorizeRequest,
|
||||
MessageTypes,
|
||||
MetadataRequest,
|
||||
RequestAccountCreateExternal,
|
||||
RequestAccountCreateSuri,
|
||||
RequestAccountEdit,
|
||||
RequestAccountExport,
|
||||
RequestAccountValidate,
|
||||
RequestAuthorizeApprove,
|
||||
RequestAuthorizeReject,
|
||||
RequestDeriveCreate,
|
||||
ResponseDeriveValidate,
|
||||
RequestMetadataApprove,
|
||||
RequestMetadataReject,
|
||||
RequestSigningApprovePassword,
|
||||
RequestSigningApproveSignature,
|
||||
RequestSigningCancel,
|
||||
RequestSeedCreate,
|
||||
RequestTypes,
|
||||
ResponseAccountExport,
|
||||
RequestAccountForget,
|
||||
ResponseSeedCreate,
|
||||
RequestSeedValidate,
|
||||
RequestDeriveValidate,
|
||||
ResponseSeedValidate,
|
||||
ResponseType,
|
||||
SigningRequest
|
||||
} from '../types';
|
||||
|
||||
import extension from 'extensionizer';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
|
||||
import { TypeRegistry } from '@polkadot/types';
|
||||
import { KeyringPair, KeyringPair$Meta } from '@polkadot/keyring/types';
|
||||
import { assert, isHex } from '@polkadot/util';
|
||||
import { keyExtractSuri, mnemonicGenerate, mnemonicValidate } from '@polkadot/util-crypto';
|
||||
|
||||
@ -62,6 +92,15 @@ export default class Extension {
|
||||
return { exportedJson: JSON.stringify(keyring.backupAccount(keyring.getPair(address), password)) };
|
||||
}
|
||||
|
||||
private accountsValidate ({ address, password }: RequestAccountValidate): boolean {
|
||||
try {
|
||||
keyring.backupAccount(keyring.getPair(address), password);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private accountsForget ({ address }: RequestAccountForget): boolean {
|
||||
keyring.forgetAccount(address);
|
||||
|
||||
@ -265,9 +304,40 @@ export default class Extension {
|
||||
return true;
|
||||
}
|
||||
|
||||
private derive (parentAddress: string, suri: string, password: string, metadata: KeyringPair$Meta): KeyringPair {
|
||||
const parentPair = keyring.getPair(parentAddress);
|
||||
|
||||
try {
|
||||
parentPair.decodePkcs8(password);
|
||||
} catch (e) {
|
||||
throw new Error('invalid password');
|
||||
}
|
||||
try {
|
||||
return parentPair.derive(suri, metadata);
|
||||
} catch (err) {
|
||||
throw new Error(`"${suri}" is not a valid derivation path`);
|
||||
}
|
||||
}
|
||||
|
||||
private derivationValidate ({ parentAddress, suri, parentPassword }: RequestDeriveValidate): ResponseDeriveValidate {
|
||||
const childPair = this.derive(parentAddress, suri, parentPassword, {});
|
||||
|
||||
return {
|
||||
address: childPair.address,
|
||||
suri
|
||||
};
|
||||
}
|
||||
|
||||
private derivationCreate ({ parentAddress, suri, parentPassword, genesisHash, name, password }: RequestDeriveCreate): boolean {
|
||||
const childPair = this.derive(parentAddress, suri, parentPassword, { genesisHash, name, parentAddress });
|
||||
keyring.addPair(childPair, password);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Weird thought, the eslint override is not needed in Tabs
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
public async handle<TMessageType extends MessageTypes> (id: string, type: TMessageType, request: RequestTypes[TMessageType], port: chrome.runtime.Port): Promise<ResponseTypes[keyof ResponseTypes]> {
|
||||
public async handle<TMessageType extends MessageTypes> (id: string, type: TMessageType, request: RequestTypes[TMessageType], port: chrome.runtime.Port): Promise<ResponseType<TMessageType>> {
|
||||
switch (type) {
|
||||
case 'pri(authorize.approve)':
|
||||
return this.authorizeApprove(request as RequestAuthorizeApprove);
|
||||
@ -293,6 +363,9 @@ export default class Extension {
|
||||
case 'pri(accounts.forget)':
|
||||
return this.accountsForget(request as RequestAccountForget);
|
||||
|
||||
case 'pri(accounts.validate)':
|
||||
return this.accountsValidate(request as RequestAccountValidate);
|
||||
|
||||
case 'pri(accounts.subscribe)':
|
||||
return this.accountsSubscribe(id, port);
|
||||
|
||||
@ -308,6 +381,12 @@ export default class Extension {
|
||||
case 'pri(metadata.requests)':
|
||||
return this.metadataSubscribe(id, port);
|
||||
|
||||
case 'pri(derivation.create)':
|
||||
return this.derivationCreate(request as RequestDeriveCreate);
|
||||
|
||||
case 'pri(derivation.validate)':
|
||||
return this.derivationValidate(request as RequestDeriveValidate);
|
||||
|
||||
case 'pri(seed.create)':
|
||||
return this.seedCreate(request as RequestSeedCreate);
|
||||
|
||||
|
||||
@ -57,10 +57,13 @@ export interface RequestSignatures {
|
||||
'pri(accounts.edit)': [RequestAccountEdit, boolean];
|
||||
'pri(accounts.export)': [RequestAccountExport, ResponseAccountExport];
|
||||
'pri(accounts.forget)': [RequestAccountForget, boolean];
|
||||
'pri(accounts.validate)': [RequestAccountValidate, boolean];
|
||||
'pri(accounts.subscribe)': [RequestAccountSubscribe, boolean, AccountJson[]];
|
||||
'pri(authorize.approve)': [RequestAuthorizeApprove, boolean];
|
||||
'pri(authorize.reject)': [RequestAuthorizeReject, boolean];
|
||||
'pri(authorize.requests)': [RequestAuthorizeSubscribe, boolean, AuthorizeRequest[]];
|
||||
'pri(derivation.create)': [RequestDeriveCreate, boolean];
|
||||
'pri(derivation.validate)': [RequestDeriveValidate, ResponseDeriveValidate];
|
||||
'pri(metadata.approve)': [RequestMetadataApprove, boolean];
|
||||
'pri(metadata.reject)': [RequestMetadataReject, boolean];
|
||||
'pri(metadata.requests)': [RequestMetadataSubscribe, boolean, MetadataRequest[]];
|
||||
@ -153,6 +156,26 @@ export interface RequestAccountForget {
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface RequestAccountValidate {
|
||||
address: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RequestDeriveCreate {
|
||||
name: string;
|
||||
genesisHash?: string | null;
|
||||
suri: string;
|
||||
parentAddress: string;
|
||||
parentPassword: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RequestDeriveValidate {
|
||||
suri: string;
|
||||
parentAddress: string;
|
||||
parentPassword: string;
|
||||
}
|
||||
|
||||
export interface RequestAccountExport {
|
||||
address: string;
|
||||
password: string;
|
||||
@ -210,6 +233,8 @@ export type ResponseTypes = {
|
||||
[MessageType in keyof RequestSignatures]: RequestSignatures[MessageType][1]
|
||||
};
|
||||
|
||||
export type ResponseType<TMessageType extends keyof RequestSignatures> = RequestSignatures[TMessageType][1];
|
||||
|
||||
interface TransportResponseMessageSub<TMessageType extends MessageTypesWithSubscriptions> {
|
||||
error?: string;
|
||||
id: string;
|
||||
@ -235,6 +260,11 @@ export interface ResponseSigning {
|
||||
signature: string;
|
||||
}
|
||||
|
||||
export interface ResponseDeriveValidate {
|
||||
address: string;
|
||||
suri: string;
|
||||
}
|
||||
|
||||
export interface ResponseSeedCreate {
|
||||
address: string;
|
||||
seed: string;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@polkadot/extension-chains",
|
||||
"description": "Definitions for all known chains as exposed by the extension.",
|
||||
"version": "0.24.0-beta.1",
|
||||
"version": "0.24.0-beta.2",
|
||||
"author": "Jaco Greeff <jacogr@gmail.com>",
|
||||
"license": "Apache-2",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@polkadot/extension-dapp",
|
||||
"description": "Provides an interfaces around the injected globals for ease of access by dapp developers.",
|
||||
"version": "0.24.0-beta.1",
|
||||
"version": "0.24.0-beta.2",
|
||||
"author": "Jaco Greeff <jacogr@gmail.com>",
|
||||
"license": "Apache-2",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@polkadot/extension-inject": "0.24.0-beta.1"
|
||||
"@polkadot/extension-inject": "0.24.0-beta.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/api": "*"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@polkadot/extension-inject",
|
||||
"description": "A generic injector (usable to any extension), that populates the base exposed interfaces to be used by dapps.",
|
||||
"version": "0.24.0-beta.1",
|
||||
"version": "0.24.0-beta.2",
|
||||
"author": "Jaco Greeff <jacogr@gmail.com>",
|
||||
"license": "Apache-2",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@polkadot/extension-ui",
|
||||
"description": "A sample signer extension for the @polkadot/api",
|
||||
"version": "0.24.0-beta.1",
|
||||
"version": "0.24.0-beta.2",
|
||||
"author": "Jaco Greeff <jacogr@gmail.com>",
|
||||
"license": "Apache-2",
|
||||
"dependencies": {
|
||||
|
||||
@ -31,10 +31,11 @@ describe('Account component', () => {
|
||||
wrapper = mountAccountComponent({ isExternal: false });
|
||||
wrapper.find('Details').simulate('click');
|
||||
|
||||
expect(wrapper.find('MenuItem').length).toBe(3);
|
||||
expect(wrapper.find('MenuItem').length).toBe(4);
|
||||
expect(wrapper.find('MenuItem').at(0).text()).toBe('Rename');
|
||||
expect(wrapper.find('MenuItem').at(1).text()).toBe('Export Account');
|
||||
expect(wrapper.find('MenuItem').at(2).text()).toBe('Forget Account');
|
||||
expect(wrapper.find('MenuItem').at(1).text()).toBe('Derive New Account');
|
||||
expect(wrapper.find('MenuItem').at(2).text()).toBe('Export Account');
|
||||
expect(wrapper.find('MenuItem').at(3).text()).toBe('Forget Account');
|
||||
});
|
||||
|
||||
it('does not show Export option if account is external', () => {
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import { AccountJson } from '@polkadot/extension-base/background/types';
|
||||
|
||||
import React, { useContext, useState } from 'react';
|
||||
import React, { useCallback, useContext, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ActionContext, Address, Link } from '../../components';
|
||||
@ -21,8 +21,8 @@ function Account ({ address, className, isExternal }: Props): React.ReactElement
|
||||
const [isEditing, setEditing] = useState(false);
|
||||
const [editedName, setName] = useState<string | null>(null);
|
||||
|
||||
const _toggleEdit = (): void => setEditing(!isEditing);
|
||||
const _saveChanges = (): void => {
|
||||
const _toggleEdit = useCallback((): void => setEditing(!isEditing), [isEditing]);
|
||||
const _saveChanges = useCallback((): void => {
|
||||
if (editedName && editedName !== name) {
|
||||
editAccount(address, editedName)
|
||||
.then((): void => onAction())
|
||||
@ -30,7 +30,7 @@ function Account ({ address, className, isExternal }: Props): React.ReactElement
|
||||
}
|
||||
|
||||
_toggleEdit();
|
||||
};
|
||||
}, [editedName, address, _toggleEdit, onAction]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
@ -41,6 +41,7 @@ function Account ({ address, className, isExternal }: Props): React.ReactElement
|
||||
<>
|
||||
<MenuGroup>
|
||||
<MenuItem onClick={_toggleEdit}>Rename</MenuItem>
|
||||
{!isExternal && <MenuItem to={`/account/derive/${address}`}>Derive New Account</MenuItem>}
|
||||
</MenuGroup>
|
||||
{!isExternal && <MenuItem isDanger to={`/account/export/${address}`}>Export Account</MenuItem>}
|
||||
<MenuItem isDanger to={`/account/forget/${address}`}>Forget Account</MenuItem>
|
||||
@ -51,7 +52,7 @@ function Account ({ address, className, isExternal }: Props): React.ReactElement
|
||||
<Name
|
||||
address={address}
|
||||
className='edit-name'
|
||||
isFocussed
|
||||
isFocused
|
||||
label={' '}
|
||||
onBlur={_saveChanges}
|
||||
onChange={setName}
|
||||
|
||||
@ -18,7 +18,7 @@ function AccountName ({ onCreate, address }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<>
|
||||
<Name
|
||||
isFocussed
|
||||
isFocused
|
||||
onChange={setName}
|
||||
/>
|
||||
{name && <Password onChange={setPassword} />}
|
||||
|
||||
41
packages/extension-ui/src/Popup/Derive/Step.tsx
Normal file
41
packages/extension-ui/src/Popup/Derive/Step.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Header } from '@polkadot/extension-ui/components';
|
||||
|
||||
interface Props {
|
||||
step: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Step ({ step, className }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<Header text={
|
||||
<>
|
||||
<span>Derive new account</span>
|
||||
<CurrentStep>{step}</CurrentStep>
|
||||
<TotalSteps>/2</TotalSteps>
|
||||
</>
|
||||
} showBackArrow className={className} />
|
||||
);
|
||||
}
|
||||
|
||||
const CurrentStep = styled.span`
|
||||
font-size: ${({ theme }): string => theme.labelFontSize};
|
||||
line-height: ${({ theme }): string => theme.labelLineHeight};
|
||||
color: ${({ theme }): string => theme.primaryColor};
|
||||
font-weight: 600;
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
const TotalSteps = styled.span`
|
||||
font-size: ${({ theme }): string => theme.labelFontSize};
|
||||
line-height: ${({ theme }): string => theme.labelLineHeight};
|
||||
color: ${({ theme }): string => theme.textColor};
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
export default Step;
|
||||
82
packages/extension-ui/src/Popup/Derive/index.tsx
Normal file
82
packages/extension-ui/src/Popup/Derive/index.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React, { useCallback, useContext, useState } from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ActionContext, Address, Button, ButtonArea, InputWithLabel, VerticalSpace } from '../../components';
|
||||
import { deriveAccount, validateAccount } from '../../messaging';
|
||||
import { DerivationPath, Name, Password } from '../../partials';
|
||||
import Step from './Step';
|
||||
|
||||
type Props = RouteComponentProps<{ address: string }>;
|
||||
|
||||
export default function Derive ({ match: { params: { address: parentAddress } } }: Props): React.ReactElement<Props> {
|
||||
const onAction = useContext(ActionContext);
|
||||
const [account, setAccount] = useState<null | { address: string; suri: string }>(null);
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
const [password, setPassword] = useState<string | null>(null);
|
||||
const [parentPassword, setParentPassword] = useState<string | null>(null);
|
||||
const [isProperParentPassword, setIsProperParentPassword] = useState(false);
|
||||
const [derivationConfirmed, setDerivationConfirmed] = useState(false);
|
||||
|
||||
const _onCreate = useCallback(async () => {
|
||||
if (!account || !name || !password || !parentPassword) {
|
||||
return;
|
||||
}
|
||||
await deriveAccount(parentAddress, account.suri, parentPassword, name, password);
|
||||
onAction('/');
|
||||
}, [account, name, password, onAction, parentAddress, parentPassword]);
|
||||
|
||||
const _onParentPasswordEnter = useCallback(async (enteredPassword: string) => {
|
||||
setParentPassword(enteredPassword);
|
||||
setIsProperParentPassword(await validateAccount(parentAddress, enteredPassword));
|
||||
}, [parentAddress]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Step step={derivationConfirmed ? 2 : 1} />
|
||||
{!derivationConfirmed && <InputWithLabel
|
||||
isFocused
|
||||
isError={!isProperParentPassword}
|
||||
label='enter the password for the account you want to derive from'
|
||||
onChange={_onParentPasswordEnter}
|
||||
type='password'
|
||||
data-export-password
|
||||
/>}
|
||||
{!derivationConfirmed && <DeriveButton
|
||||
isDisabled={!isProperParentPassword}
|
||||
onClick={(): void => setDerivationConfirmed(true)}
|
||||
>
|
||||
I want to derive from this account
|
||||
</DeriveButton>}
|
||||
{isProperParentPassword && derivationConfirmed && <Name onChange={setName} isFocused />}
|
||||
{isProperParentPassword && derivationConfirmed && parentPassword && name && <DerivationPath
|
||||
onChange={setAccount}
|
||||
parentAddress={parentAddress}
|
||||
parentPassword={parentPassword}
|
||||
/>}
|
||||
{isProperParentPassword && derivationConfirmed && account && name && <Password onChange={setPassword}/>}
|
||||
{isProperParentPassword && derivationConfirmed && account && name && password && (
|
||||
<>
|
||||
<Address
|
||||
address={account.address}
|
||||
name={name}
|
||||
/>
|
||||
<VerticalSpace/>
|
||||
<ButtonArea>
|
||||
<Button onClick={_onCreate}>Create derived account</Button>
|
||||
</ButtonArea>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const DeriveButton = styled(Button)`
|
||||
margin-left: 24px;
|
||||
margin-right: 24px;
|
||||
width: auto;
|
||||
`;
|
||||
@ -36,7 +36,7 @@ export default function ImportQr (): React.ReactElement<Props> {
|
||||
{account && (
|
||||
<>
|
||||
<Name
|
||||
isFocussed
|
||||
isFocused
|
||||
onChange={setName}
|
||||
/>
|
||||
<Address
|
||||
|
||||
@ -15,6 +15,7 @@ import { subscribeAccounts, subscribeAuthorizeRequests, subscribeMetadataRequest
|
||||
import Accounts from './Accounts';
|
||||
import Authorize from './Authorize';
|
||||
import CreateAccount from './CreateAccount';
|
||||
import Derive from './Derive';
|
||||
import Export from './Export';
|
||||
import Forget from './Forget';
|
||||
import ImportQr from './ImportQr';
|
||||
@ -105,6 +106,7 @@ export default function Popup (): React.ReactElement<{}> {
|
||||
<Route path='/account/export/:address' component={Export} />
|
||||
<Route path='/account/import-qr' component={ImportQr} />
|
||||
<Route path='/account/import-seed' component={ImportSeed} />
|
||||
<Route path='/account/derive/:address' component={Derive} />
|
||||
<Route exact path='/' component={Root} />
|
||||
</Switch>
|
||||
</SigningReqContext.Provider>
|
||||
|
||||
@ -17,7 +17,7 @@ interface Props {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
showSettings?: boolean;
|
||||
text?: string;
|
||||
text?: React.ReactNode;
|
||||
showBackArrow?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@ -19,9 +19,10 @@ interface Props {
|
||||
onChange?: (value: string) => void;
|
||||
type?: 'text' | 'password';
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
function InputWithLabel ({ className, defaultValue, label, isFocused, isReadOnly, onBlur, onChange, type = 'text', value, isError }: Props): React.ReactElement<Props> {
|
||||
function InputWithLabel ({ className, defaultValue, label, isError, isFocused, isReadOnly, onBlur, onChange, placeholder, type = 'text', value }: Props): React.ReactElement<Props> {
|
||||
const _onChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
onChange && onChange(value.trim());
|
||||
};
|
||||
@ -43,6 +44,7 @@ function InputWithLabel ({ className, defaultValue, label, isFocused, isReadOnly
|
||||
type={type}
|
||||
value={value}
|
||||
withError={isError}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</Label>
|
||||
);
|
||||
|
||||
@ -2,7 +2,21 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { AccountJson, AuthorizeRequest, SigningRequest, RequestTypes, MessageTypes, ResponseTypes, SeedLengths, SubscriptionMessageTypes, MetadataRequest, MessageTypesWithNullRequest, MessageTypesWithNoSubscriptions, MessageTypesWithSubscriptions } from '@polkadot/extension-base/background/types';
|
||||
import {
|
||||
AccountJson,
|
||||
AuthorizeRequest,
|
||||
SigningRequest,
|
||||
RequestTypes,
|
||||
MessageTypes,
|
||||
ResponseTypes,
|
||||
SeedLengths,
|
||||
SubscriptionMessageTypes,
|
||||
MetadataRequest,
|
||||
MessageTypesWithNullRequest,
|
||||
MessageTypesWithNoSubscriptions,
|
||||
MessageTypesWithSubscriptions,
|
||||
ResponseDeriveValidate
|
||||
} from '@polkadot/extension-base/background/types';
|
||||
import { Chain } from '@polkadot/extension-chains/types';
|
||||
import { KeypairType } from '@polkadot/util-crypto/types';
|
||||
|
||||
@ -68,6 +82,10 @@ export async function exportAccount (address: string, password: string): Promise
|
||||
return sendMessage('pri(accounts.export)', { address, password });
|
||||
}
|
||||
|
||||
export async function validateAccount (address: string, password: string): Promise<boolean> {
|
||||
return sendMessage('pri(accounts.validate)', { address, password });
|
||||
}
|
||||
|
||||
export async function forgetAccount (address: string): Promise<boolean> {
|
||||
return sendMessage('pri(accounts.forget)', { address });
|
||||
}
|
||||
@ -141,3 +159,11 @@ export async function validateSeed (suri: string, type?: KeypairType): Promise<{
|
||||
export async function windowOpen (): Promise<boolean> {
|
||||
return sendMessage('pri(window.open)', null);
|
||||
}
|
||||
|
||||
export async function validateDerivationPath (parentAddress: string, suri: string, parentPassword: string): Promise<ResponseDeriveValidate> {
|
||||
return sendMessage('pri(derivation.validate)', { parentAddress, suri, parentPassword });
|
||||
}
|
||||
|
||||
export async function deriveAccount (parentAddress: string, suri: string, parentPassword: string, name: string, password: string): Promise<boolean> {
|
||||
return sendMessage('pri(derivation.create)', { parentAddress, suri, parentPassword, name, password });
|
||||
}
|
||||
|
||||
43
packages/extension-ui/src/partials/DerivationPath.tsx
Normal file
43
packages/extension-ui/src/partials/DerivationPath.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { InputWithLabel } from '@polkadot/extension-ui/components';
|
||||
import { validateDerivationPath } from '@polkadot/extension-ui/messaging';
|
||||
|
||||
interface Props {
|
||||
onChange: (derivedAccount: { address: string; suri: string } | null) => void;
|
||||
parentAddress: string;
|
||||
parentPassword: string;
|
||||
}
|
||||
|
||||
function DerivationPath ({ onChange, parentAddress, parentPassword }: Props): React.ReactElement<Props> {
|
||||
const [path, setPath] = useState<string>('');
|
||||
const [isValid, setValid] = useState(false);
|
||||
|
||||
const _onChange = useCallback(async (newPath: string) => {
|
||||
setPath(newPath);
|
||||
try {
|
||||
onChange(await validateDerivationPath(parentAddress, newPath, parentPassword));
|
||||
setValid(true);
|
||||
} catch (err) {
|
||||
onChange(null);
|
||||
setValid(false);
|
||||
}
|
||||
}, [parentAddress, onChange, parentPassword]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InputWithLabel
|
||||
label='Derivation path'
|
||||
placeholder='//hard/soft'
|
||||
onChange={_onChange}
|
||||
value={path}
|
||||
isError={!isValid}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DerivationPath;
|
||||
@ -10,7 +10,7 @@ interface Props {
|
||||
address?: string;
|
||||
className?: string;
|
||||
defaultValue?: string | null;
|
||||
isFocussed?: boolean;
|
||||
isFocused?: boolean;
|
||||
label?: string;
|
||||
onBlur?: () => void;
|
||||
onChange: (name: string | null) => void;
|
||||
@ -18,7 +18,7 @@ interface Props {
|
||||
|
||||
const MIN_LENGTH = 3;
|
||||
|
||||
export default function Name ({ address, className, defaultValue, isFocussed, label = 'A descriptive name for your account', onBlur, onChange }: Props): React.ReactElement<Props> {
|
||||
export default function Name ({ address, className, defaultValue, isFocused, label = 'A descriptive name for your account', onBlur, onChange }: Props): React.ReactElement<Props> {
|
||||
const accounts = useContext(AccountContext);
|
||||
const [name, setName] = useState('');
|
||||
const account = accounts.find((account): boolean => account.address === address);
|
||||
@ -41,7 +41,7 @@ export default function Name ({ address, className, defaultValue, isFocussed, la
|
||||
className={className}
|
||||
defaultValue={startValue}
|
||||
isError={isError}
|
||||
isFocused={isFocussed}
|
||||
isFocused={isFocused}
|
||||
label={label}
|
||||
onBlur={onBlur}
|
||||
onChange={setName}
|
||||
|
||||
@ -5,3 +5,4 @@
|
||||
export { default as Back } from './Back';
|
||||
export { default as Name } from './Name';
|
||||
export { default as Password } from './Password';
|
||||
export { default as DerivationPath } from './DerivationPath';
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@polkadot/extension",
|
||||
"description": "A sample signer extension for the @polkadot/api",
|
||||
"version": "0.24.0-beta.1",
|
||||
"version": "0.24.0-beta.2",
|
||||
"author": "Jaco Greeff <jacogr@gmail.com>",
|
||||
"license": "Apache-2",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@polkadot/extension-base": "0.24.0-beta.1",
|
||||
"@polkadot/extension-ui": "0.24.0-beta.1"
|
||||
"@polkadot/extension-base": "0.24.0-beta.2",
|
||||
"@polkadot/extension-ui": "0.24.0-beta.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@polkadot/dev": "^0.51.9",
|
||||
|
||||
12
yarn.lock
12
yarn.lock
@ -2871,7 +2871,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polkadot/extension-base@0.24.0-beta.1, @polkadot/extension-base@workspace:packages/extension-base":
|
||||
"@polkadot/extension-base@0.24.0-beta.2, @polkadot/extension-base@workspace:packages/extension-base":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/extension-base@workspace:packages/extension-base"
|
||||
dependencies:
|
||||
@ -2898,13 +2898,13 @@ __metadata:
|
||||
resolution: "@polkadot/extension-dapp@workspace:packages/extension-dapp"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.9.2
|
||||
"@polkadot/extension-inject": 0.24.0-beta.1
|
||||
"@polkadot/extension-inject": 0.24.0-beta.2
|
||||
peerDependencies:
|
||||
"@polkadot/api": "*"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@polkadot/extension-inject@0.24.0-beta.1, @polkadot/extension-inject@workspace:packages/extension-inject":
|
||||
"@polkadot/extension-inject@0.24.0-beta.2, @polkadot/extension-inject@workspace:packages/extension-inject":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/extension-inject@workspace:packages/extension-inject"
|
||||
dependencies:
|
||||
@ -2914,7 +2914,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@polkadot/extension-ui@0.24.0-beta.1, @polkadot/extension-ui@workspace:packages/extension-ui":
|
||||
"@polkadot/extension-ui@0.24.0-beta.2, @polkadot/extension-ui@workspace:packages/extension-ui":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/extension-ui@workspace:packages/extension-ui"
|
||||
dependencies:
|
||||
@ -2950,8 +2950,8 @@ __metadata:
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.9.2
|
||||
"@polkadot/dev": ^0.51.9
|
||||
"@polkadot/extension-base": 0.24.0-beta.1
|
||||
"@polkadot/extension-ui": 0.24.0-beta.1
|
||||
"@polkadot/extension-base": 0.24.0-beta.2
|
||||
"@polkadot/extension-ui": 0.24.0-beta.2
|
||||
"@types/chrome": ^0.0.102
|
||||
"@types/firefox-webext-browser": ^70.0.1
|
||||
babel-loader: ^8.1.0
|
||||
|
||||
Loading…
Reference in New Issue
Block a user