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:
Bartłomiej Rutkowski 2020-03-30 16:55:53 +02:00 committed by GitHub
parent 6d83833545
commit eeac2c1064
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 423 additions and 42 deletions

1
.gitignore vendored
View File

@ -22,3 +22,4 @@ yarn-error.log*
package-lock.json
_book
docs/html
.idea

View File

@ -9,5 +9,5 @@
"packages": [
"packages/*"
],
"version": "0.24.0-beta.1"
"version": "0.24.0-beta.2"
}

View File

@ -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": {

View File

@ -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);
});
});
});

View File

@ -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);

View File

@ -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;

View File

@ -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": {

View File

@ -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": "*"

View File

@ -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": {

View File

@ -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": {

View File

@ -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', () => {

View File

@ -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}

View File

@ -18,7 +18,7 @@ function AccountName ({ onCreate, address }: Props): React.ReactElement<Props> {
return (
<>
<Name
isFocussed
isFocused
onChange={setName}
/>
{name && <Password onChange={setPassword} />}

View 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;

View 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;
`;

View File

@ -36,7 +36,7 @@ export default function ImportQr (): React.ReactElement<Props> {
{account && (
<>
<Name
isFocussed
isFocused
onChange={setName}
/>
<Address

View File

@ -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>

View File

@ -17,7 +17,7 @@ interface Props {
children?: React.ReactNode;
className?: string;
showSettings?: boolean;
text?: string;
text?: React.ReactNode;
showBackArrow?: boolean;
}

View File

@ -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>
);

View File

@ -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 });
}

View 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;

View File

@ -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}

View File

@ -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';

View File

@ -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",

View File

@ -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