diff --git a/.gitignore b/.gitignore index 424e0b5f..ed7cfb49 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ yarn-error.log* package-lock.json _book docs/html +.idea \ No newline at end of file diff --git a/lerna.json b/lerna.json index 00540912..dd4d3df9 100644 --- a/lerna.json +++ b/lerna.json @@ -9,5 +9,5 @@ "packages": [ "packages/*" ], - "version": "0.24.0-beta.1" + "version": "0.24.0-beta.2" } diff --git a/packages/extension-base/package.json b/packages/extension-base/package.json index 0b9f1f7a..2da7fbc3 100644 --- a/packages/extension-base/package.json +++ b/packages/extension-base/package.json @@ -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 ", "license": "Apache-2", "dependencies": { diff --git a/packages/extension-base/src/background/handlers/Extension.test.ts b/packages/extension-base/src/background/handlers/Extension.test.ts index c23e8a61..b0e3d9c3 100644 --- a/packages/extension-base/src/background/handlers/Extension.test.ts +++ b/packages/extension-base/src/background/handlers/Extension.test.ts @@ -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 => { + 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); + }); + }); }); diff --git a/packages/extension-base/src/background/handlers/Extension.ts b/packages/extension-base/src/background/handlers/Extension.ts index 0c40ff18..944250c5 100644 --- a/packages/extension-base/src/background/handlers/Extension.ts +++ b/packages/extension-base/src/background/handlers/Extension.ts @@ -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 (id: string, type: TMessageType, request: RequestTypes[TMessageType], port: chrome.runtime.Port): Promise { + public async handle (id: string, type: TMessageType, request: RequestTypes[TMessageType], port: chrome.runtime.Port): Promise> { 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); diff --git a/packages/extension-base/src/background/types.ts b/packages/extension-base/src/background/types.ts index 980afa37..f7aacf78 100644 --- a/packages/extension-base/src/background/types.ts +++ b/packages/extension-base/src/background/types.ts @@ -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 = RequestSignatures[TMessageType][1]; + interface TransportResponseMessageSub { 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; diff --git a/packages/extension-chains/package.json b/packages/extension-chains/package.json index 8894bb0f..1b38db2c 100644 --- a/packages/extension-chains/package.json +++ b/packages/extension-chains/package.json @@ -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 ", "license": "Apache-2", "dependencies": { diff --git a/packages/extension-dapp/package.json b/packages/extension-dapp/package.json index cafaad7b..2ed6fd6a 100644 --- a/packages/extension-dapp/package.json +++ b/packages/extension-dapp/package.json @@ -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 ", "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": "*" diff --git a/packages/extension-inject/package.json b/packages/extension-inject/package.json index cdc42420..283bc011 100644 --- a/packages/extension-inject/package.json +++ b/packages/extension-inject/package.json @@ -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 ", "license": "Apache-2", "dependencies": { diff --git a/packages/extension-ui/package.json b/packages/extension-ui/package.json index 1d017e0b..8a70894a 100644 --- a/packages/extension-ui/package.json +++ b/packages/extension-ui/package.json @@ -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 ", "license": "Apache-2", "dependencies": { diff --git a/packages/extension-ui/src/Popup/Accounts/Account.test.tsx b/packages/extension-ui/src/Popup/Accounts/Account.test.tsx index 99b9e8f6..b56fa9a5 100644 --- a/packages/extension-ui/src/Popup/Accounts/Account.test.tsx +++ b/packages/extension-ui/src/Popup/Accounts/Account.test.tsx @@ -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', () => { diff --git a/packages/extension-ui/src/Popup/Accounts/Account.tsx b/packages/extension-ui/src/Popup/Accounts/Account.tsx index 2dc021c5..dd06128b 100644 --- a/packages/extension-ui/src/Popup/Accounts/Account.tsx +++ b/packages/extension-ui/src/Popup/Accounts/Account.tsx @@ -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(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 (
@@ -41,6 +41,7 @@ function Account ({ address, className, isExternal }: Props): React.ReactElement <> Rename + {!isExternal && Derive New Account} {!isExternal && Export Account} Forget Account @@ -51,7 +52,7 @@ function Account ({ address, className, isExternal }: Props): React.ReactElement { return ( <> {name && } diff --git a/packages/extension-ui/src/Popup/Derive/Step.tsx b/packages/extension-ui/src/Popup/Derive/Step.tsx new file mode 100644 index 00000000..f6218a8e --- /dev/null +++ b/packages/extension-ui/src/Popup/Derive/Step.tsx @@ -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 { + return ( +
+ Derive new account + {step} + /2 + + } 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; diff --git a/packages/extension-ui/src/Popup/Derive/index.tsx b/packages/extension-ui/src/Popup/Derive/index.tsx new file mode 100644 index 00000000..72c31e98 --- /dev/null +++ b/packages/extension-ui/src/Popup/Derive/index.tsx @@ -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 { + const onAction = useContext(ActionContext); + const [account, setAccount] = useState(null); + const [name, setName] = useState(null); + const [password, setPassword] = useState(null); + const [parentPassword, setParentPassword] = useState(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 ( + <> + + {!derivationConfirmed && } + {!derivationConfirmed && setDerivationConfirmed(true)} + > + I want to derive from this account + } + {isProperParentPassword && derivationConfirmed && } + {isProperParentPassword && derivationConfirmed && parentPassword && name && } + {isProperParentPassword && derivationConfirmed && account && name && } + {isProperParentPassword && derivationConfirmed && account && name && password && ( + <> +
+ + + + + + )} + + ); +} + +const DeriveButton = styled(Button)` + margin-left: 24px; + margin-right: 24px; + width: auto; +`; diff --git a/packages/extension-ui/src/Popup/ImportQr.tsx b/packages/extension-ui/src/Popup/ImportQr.tsx index 8d852f45..22667fad 100644 --- a/packages/extension-ui/src/Popup/ImportQr.tsx +++ b/packages/extension-ui/src/Popup/ImportQr.tsx @@ -36,7 +36,7 @@ export default function ImportQr (): React.ReactElement { {account && ( <>
{ + diff --git a/packages/extension-ui/src/components/Header.tsx b/packages/extension-ui/src/components/Header.tsx index b91037df..178a06b7 100644 --- a/packages/extension-ui/src/components/Header.tsx +++ b/packages/extension-ui/src/components/Header.tsx @@ -17,7 +17,7 @@ interface Props { children?: React.ReactNode; className?: string; showSettings?: boolean; - text?: string; + text?: React.ReactNode; showBackArrow?: boolean; } diff --git a/packages/extension-ui/src/components/InputWithLabel.tsx b/packages/extension-ui/src/components/InputWithLabel.tsx index 682f24d6..77767589 100644 --- a/packages/extension-ui/src/components/InputWithLabel.tsx +++ b/packages/extension-ui/src/components/InputWithLabel.tsx @@ -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 { +function InputWithLabel ({ className, defaultValue, label, isError, isFocused, isReadOnly, onBlur, onChange, placeholder, type = 'text', value }: Props): React.ReactElement { const _onChange = ({ target: { value } }: React.ChangeEvent): void => { onChange && onChange(value.trim()); }; @@ -43,6 +44,7 @@ function InputWithLabel ({ className, defaultValue, label, isFocused, isReadOnly type={type} value={value} withError={isError} + placeholder={placeholder} /> ); diff --git a/packages/extension-ui/src/messaging.ts b/packages/extension-ui/src/messaging.ts index 8fcf8954..87607b53 100644 --- a/packages/extension-ui/src/messaging.ts +++ b/packages/extension-ui/src/messaging.ts @@ -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 { + return sendMessage('pri(accounts.validate)', { address, password }); +} + export async function forgetAccount (address: string): Promise { return sendMessage('pri(accounts.forget)', { address }); } @@ -141,3 +159,11 @@ export async function validateSeed (suri: string, type?: KeypairType): Promise<{ export async function windowOpen (): Promise { return sendMessage('pri(window.open)', null); } + +export async function validateDerivationPath (parentAddress: string, suri: string, parentPassword: string): Promise { + return sendMessage('pri(derivation.validate)', { parentAddress, suri, parentPassword }); +} + +export async function deriveAccount (parentAddress: string, suri: string, parentPassword: string, name: string, password: string): Promise { + return sendMessage('pri(derivation.create)', { parentAddress, suri, parentPassword, name, password }); +} diff --git a/packages/extension-ui/src/partials/DerivationPath.tsx b/packages/extension-ui/src/partials/DerivationPath.tsx new file mode 100644 index 00000000..7d5ded4b --- /dev/null +++ b/packages/extension-ui/src/partials/DerivationPath.tsx @@ -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 { + const [path, setPath] = useState(''); + 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 ( +
+ +
+ ); +} + +export default DerivationPath; diff --git a/packages/extension-ui/src/partials/Name.tsx b/packages/extension-ui/src/partials/Name.tsx index 5f307afa..c7de4415 100644 --- a/packages/extension-ui/src/partials/Name.tsx +++ b/packages/extension-ui/src/partials/Name.tsx @@ -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 { +export default function Name ({ address, className, defaultValue, isFocused, label = 'A descriptive name for your account', onBlur, onChange }: Props): React.ReactElement { 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} diff --git a/packages/extension-ui/src/partials/index.ts b/packages/extension-ui/src/partials/index.ts index 3a6a8461..633415ed 100644 --- a/packages/extension-ui/src/partials/index.ts +++ b/packages/extension-ui/src/partials/index.ts @@ -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'; diff --git a/packages/extension/package.json b/packages/extension/package.json index 0cc815b5..de55b595 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -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 ", "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", diff --git a/yarn.lock b/yarn.lock index 244e8099..fdce5831 100644 --- a/yarn.lock +++ b/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