Browse Source

added membership, role account and spinner to apply flow

Paul M Fox 5 years ago
parent
commit
24badc6f72

+ 4 - 3
packages/app-accounts/src/modals/Backup.tsx

@@ -9,6 +9,7 @@ import React from 'react';
 import { AddressRow, Button, Modal, Password, TxComponent } from '@polkadot/react-components';
 import { ActionStatus } from '@polkadot/react-components/Status/types';
 import keyring from '@polkadot/ui-keyring';
+import { isPasswordValid } from '@polkadot/joy-utils/accounts';
 
 import translate from '../translate';
 
@@ -24,7 +25,7 @@ interface State {
 
 class Backup extends TxComponent<Props, State> {
   public state: State = {
-    isPassValid: false,
+    isPassValid: true,
     password: ''
   };
 
@@ -121,7 +122,7 @@ class Backup extends TxComponent<Props, State> {
 
       FileSaver.saveAs(blob, `${address}.json`);
     } catch (error) {
-      this.setState({ isPassValid: false });
+      this.setState({ isPassValid: true });
       console.error(error);
 
       status.status = 'error';
@@ -134,7 +135,7 @@ class Backup extends TxComponent<Props, State> {
 
   private onChangePass = (password: string): void => {
     this.setState({
-      isPassValid: keyring.isPassValid(password),
+      isPassValid: isPasswordValid(password),
       password
     });
   }

+ 2 - 1
packages/app-accounts/src/modals/ChangePass.tsx

@@ -8,6 +8,7 @@ import React from 'react';
 import { AddressRow, Button, Modal, Password, TxComponent } from '@polkadot/react-components';
 import { ActionStatus } from '@polkadot/react-components/Status/types';
 import keyring from '@polkadot/ui-keyring';
+import { isPasswordValid } from '@polkadot/joy-utils/accounts';
 
 import translate from '../translate';
 
@@ -175,7 +176,7 @@ class ChangePass extends TxComponent<Props, State> {
   }
 
   private validatePass (password: string): boolean {
-    return keyring.isPassValid(password);
+    return isPasswordValid(password);
   }
 }
 

+ 3 - 132
packages/app-accounts/src/modals/Create.tsx

@@ -3,22 +3,16 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 import { I18nProps } from '@polkadot/react-components/types';
-import { ActionStatus } from '@polkadot/react-components/Status/types';
-import { CreateResult } from '@polkadot/ui-keyring/types';
 import { KeypairType } from '@polkadot/util-crypto/types';
 import { ModalProps } from '../types';
 
 import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
-import FileSaver from 'file-saver';
+import { generateSeed, updateAddress, createAccount, isPasswordValid, AddressState, SeedType } from '@polkadot/joy-utils/accounts';
 import React, { useContext, useState } from 'react';
 import styled from 'styled-components';
-import { DEV_PHRASE } from '@polkadot/keyring/defaults';
 import { ApiContext } from '@polkadot/react-api';
-import { AddressRow, Button, Dropdown, Input, InputAddress, Modal, Password } from '@polkadot/react-components';
-import keyring from '@polkadot/ui-keyring';
+import { AddressRow, Button, Dropdown, Input, Modal, Password } from '@polkadot/react-components';
 import uiSettings from '@polkadot/ui-settings';
-import { isHex, u8aToHex } from '@polkadot/util';
-import { keyExtractSuri, mnemonicGenerate, mnemonicValidate, randomAsU8a } from '@polkadot/util-crypto';
 
 import translate from '../translate';
 import CreateConfirmation from './CreateConfirmation';
@@ -28,129 +22,6 @@ interface Props extends ModalProps, I18nProps {
   type?: KeypairType;
 }
 
-type SeedType = 'bip' | 'raw' | 'dev';
-
-interface AddressState {
-  address: string | null;
-  deriveError: string | null;
-  derivePath: string;
-  isSeedValid: boolean;
-  pairType: KeypairType;
-  seed: string;
-  seedType: SeedType;
-}
-
-const DEFAULT_PAIR_TYPE = 'ed25519'; // 'sr25519';
-
-function deriveValidate (seed: string, derivePath: string, pairType: KeypairType): string | null {
-  try {
-    const { path } = keyExtractSuri(`${seed}${derivePath}`);
-
-    // we don't allow soft for ed25519
-    if (pairType === 'ed25519' && path.some(({ isSoft }): boolean => isSoft)) {
-      return 'Soft derivation paths are not allowed on ed25519';
-    }
-  } catch (error) {
-    return error.message;
-  }
-
-  return null;
-}
-
-function isHexSeed (seed: string): boolean {
-  return isHex(seed) && seed.length === 66;
-}
-
-function rawValidate (seed: string): boolean {
-  return ((seed.length > 0) && (seed.length <= 32)) || isHexSeed(seed);
-}
-
-function addressFromSeed (phrase: string, derivePath: string, pairType: KeypairType): string {
-  return keyring
-    .createFromUri(`${phrase.trim()}${derivePath}`, {}, pairType)
-    .address;
-}
-
-function newSeed (seed: string | undefined | null, seedType: SeedType): string {
-  switch (seedType) {
-    case 'bip':
-      return mnemonicGenerate();
-    case 'dev':
-      return DEV_PHRASE;
-    default:
-      return seed || u8aToHex(randomAsU8a());
-  }
-}
-
-function generateSeed (_seed: string | undefined | null, derivePath: string, seedType: SeedType, pairType: KeypairType = DEFAULT_PAIR_TYPE): AddressState {
-  const seed = newSeed(_seed, seedType);
-  const address = addressFromSeed(seed, derivePath, pairType);
-
-  return {
-    address,
-    deriveError: null,
-    derivePath,
-    isSeedValid: true,
-    pairType,
-    seedType,
-    seed
-  };
-}
-
-function updateAddress (seed: string, derivePath: string, seedType: SeedType, pairType: KeypairType): AddressState {
-  const deriveError = deriveValidate(seed, derivePath, pairType);
-  let isSeedValid = seedType === 'raw'
-    ? rawValidate(seed)
-    : mnemonicValidate(seed);
-  let address: string | null = null;
-
-  if (!deriveError && isSeedValid) {
-    try {
-      address = addressFromSeed(seed, derivePath, pairType);
-    } catch (error) {
-      isSeedValid = false;
-    }
-  }
-
-  return {
-    address,
-    deriveError,
-    derivePath,
-    isSeedValid,
-    pairType,
-    seedType,
-    seed
-  };
-}
-
-export function downloadAccount ({ json, pair }: CreateResult): void {
-  const blob = new Blob([JSON.stringify(json)], { type: 'application/json; charset=utf-8' });
-
-  FileSaver.saveAs(blob, `${pair.address}.json`);
-  InputAddress.setLastValue('account', pair.address);
-}
-
-function createAccount (suri: string, pairType: KeypairType, name: string, password: string, success: string): ActionStatus {
-  // we will fill in all the details below
-  const status = { action: 'create' } as ActionStatus;
-
-  try {
-    const result = keyring.addUri(suri, password, { name: name.trim(), tags: [] }, pairType);
-    const { address } = result.pair;
-
-    status.account = address;
-    status.status = 'success';
-    status.message = success;
-
-    downloadAccount(result);
-  } catch (error) {
-    status.status = 'error';
-    status.message = error.message;
-  }
-
-  return status;
-}
-
 function Create ({ className, onClose, onStatusChange, seed: propsSeed, t, type: propsType }: Props): React.ReactElement<Props> {
   const { isDevelopment } = useContext(ApiContext);
   const [{ address, deriveError, derivePath, isSeedValid, pairType, seed, seedType }, setAddress] = useState<AddressState>(generateSeed(propsSeed, '', propsSeed ? 'raw' : 'bip', propsType));
@@ -160,7 +31,7 @@ function Create ({ className, onClose, onStatusChange, seed: propsSeed, t, type:
   const isValid = !!address && !deriveError && isNameValid && isPassValid && isSeedValid;
 
   const _onChangePass = (password: string): void =>
-    setPassword({ isPassValid: keyring.isPassValid(password), password });
+    setPassword({ isPassValid: isPasswordValid(password), password });
   const _onChangeDerive = (newDerivePath: string): void =>
     setAddress(updateAddress(seed, newDerivePath, seedType, pairType));
   const _onChangeSeed = (newSeed: string): void =>

+ 4 - 3
packages/app-accounts/src/modals/Derive.tsx

@@ -11,12 +11,13 @@ import React, { useContext, useEffect, useState } from 'react';
 import { AddressRow, Button, Input, InputAddress, Modal, Password, StatusContext } from '@polkadot/react-components';
 import { useDebounce } from '@polkadot/react-components/hooks';
 import keyring from '@polkadot/ui-keyring';
+import { isPasswordValid } from '@polkadot/joy-utils/accounts';
 import { keyExtractPath } from '@polkadot/util-crypto';
 
 import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
 
 import translate from '../translate';
-import { downloadAccount } from './Create';
+import { downloadAccount } from '@polkadot/joy-utils/accounts';
 import CreateConfirmation from './CreateConfirmation';
 
 interface Props extends I18nProps {
@@ -76,7 +77,7 @@ function Derive ({ className, from, onClose, t }: Props): React.ReactElement {
   const [isConfirmationOpen, setIsConfirmationOpen] = useState(false);
   const [isLocked, setIsLocked] = useState(source.isLocked);
   const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
-  const [{ isPassValid, password }, setPassword] = useState({ isPassValid: false, password: '' });
+  const [{ isPassValid, password }, setPassword] = useState({ isPassValid: true, password: '' });
   const [rootPass, setRootPass] = useState('');
   const [suri, setSuri] = useState('');
   const debouncedSuri = useDebounce(suri);
@@ -103,7 +104,7 @@ function Derive ({ className, from, onClose, t }: Props): React.ReactElement {
   }, [debouncedSuri]);
 
   const _onChangeName = (name: string): void => setName({ isNameValid: !!name.trim(), name });
-  const _onChangePass = (password: string): void => setPassword({ isPassValid: keyring.isPassValid(password), password });
+  const _onChangePass = (password: string): void => setPassword({ isPassValid: isPasswordValid(password), password });
   const _toggleConfirmation = (): void => setIsConfirmationOpen(!isConfirmationOpen);
   const _onUnlock = (): void => {
     try {

+ 5 - 4
packages/app-accounts/src/modals/Import.tsx

@@ -8,7 +8,8 @@ import { ActionStatus } from '@polkadot/react-components/Status/types';
 import { ModalProps } from '../types';
 
 import React from 'react';
-import { MyAccountContext} from '@polkadot/joy-utils/MyAccountContext';
+import { MyAccountContext } from '@polkadot/joy-utils/MyAccountContext';
+import { isPasswordValid } from '@polkadot/joy-utils/accounts';
 import { AddressRow, Button, InputAddress, InputFile, Modal, Password, TxComponent } from '@polkadot/react-components';
 import { isHex, isObject, u8aToString } from '@polkadot/util';
 import keyring from '@polkadot/ui-keyring';
@@ -32,7 +33,7 @@ class Import extends TxComponent<Props, State> {
   public state: State = {
     address: null,
     isFileValid: false,
-    isPassValid: false,
+    isPassValid: true,
     json: null,
     password: ''
   };
@@ -135,7 +136,7 @@ class Import extends TxComponent<Props, State> {
 
   private onChangePass = (password: string): void => {
     this.setState({
-      isPassValid: keyring.isPassValid(password),
+      isPassValid: isPasswordValid(password),
       password
     });
   }
@@ -161,7 +162,7 @@ class Import extends TxComponent<Props, State> {
       InputAddress.setLastValue('account', address);
       this.context.set(address)
     } catch (error) {
-      this.setState({ isPassValid: false });
+      this.setState({ isPassValid: true });
 
       status.status = 'error';
       status.message = error.message;

+ 21 - 4
packages/joy-roles/src/flows/apply.controller.tsx

@@ -35,6 +35,8 @@ type State = {
   appDetails: any
   txKeyAddress: AccountId
   activeStep: ProgressSteps
+  txInProgress: boolean
+  complete: boolean
 
   // Data generated for transaction
   transactionDetails: Map<string, string>
@@ -54,6 +56,8 @@ const newEmptyState = (): State => {
     roleKeyName: "",
     txKeyAddress: new AccountId(),
     activeStep: 0,
+    txInProgress: false,
+    complete: false,
   }
 }
 
@@ -110,6 +114,8 @@ export class ApplyController extends Controller<State, ITransport> {
             ProgressSteps.ConfirmStakes :
             ProgressSteps.ApplicationDetails
 
+          this.state.roleKeyName = hrt.job.title + " role key"
+
           // When everything is collected, update the view
           this.dispatch()
         }
@@ -149,7 +155,16 @@ export class ApplyController extends Controller<State, ITransport> {
     this.dispatch()
   }
 
-  // TODO: Move to transport
+  setTxInProgress(v: boolean): void {
+    this.state.txInProgress = v
+    this.dispatch()
+  }
+
+  setComplete(v: boolean): void {
+    this.state.complete = v
+    this.dispatch()
+  }
+
   async prepareApplicationTransaction(
     applicationStake: Balance,
     roleStake: Balance,
@@ -164,7 +179,6 @@ export class ApplyController extends Controller<State, ITransport> {
     this.state.transactionDetails.set("Application stake", formatBalance(this.state.applicationStake))
     this.state.transactionDetails.set("Role stake", formatBalance(roleStake))
     this.state.transactionDetails.set("Total commitment", formatBalance(totalCommitment))
-    this.state.roleKeyName = "some-role.key"
 
     this.dispatch()
     return true
@@ -173,8 +187,7 @@ export class ApplyController extends Controller<State, ITransport> {
   async makeApplicationTransaction(): Promise<number> {
     return this.transport.applyToCuratorOpening(
       this.currentOpeningId,
-      0, // FIXME: member id?
-      this.state.txKeyAddress.toString(), // FIXME: this should be the role ID
+      this.state.roleKeyName,
       this.state.txKeyAddress.toString(),
       this.state.applicationStake,
       this.state.roleStake,
@@ -212,6 +225,10 @@ export const ApplyView = View<ApplyController, State>(
           setTxKeyAddress={(v) => controller.setTxKeyAddress(v)}
           activeStep={state.activeStep}
           setActiveStep={(v) => controller.setActiveStep(v)}
+          txInProgress={state.txInProgress}
+          setTxInProgress={(v) => controller.setTxInProgress(v)}
+          complete={state.complete}
+          setComplete={(v) => controller.setComplete(v)}
         />
       </Container>
     )

+ 13 - 0
packages/joy-roles/src/flows/apply.sass

@@ -15,6 +15,19 @@
     height: 100%
     background: white
 
+  .loading
+    position: fixed
+    top: 0
+    left: 0
+    width: 100%
+    height: 100%
+    background: rgba(255,255,255,0.85)
+    z-index: 99999
+    display: flex
+
+    .spinner
+      margin: auto
+
   .inner
     min-height: 100%
   

+ 33 - 19
packages/joy-roles/src/flows/apply.tsx

@@ -969,7 +969,11 @@ export const SubmitApplicationStage = (props: SubmitApplicationStageProps) => {
     <Container className="content">
       <p>
         You need to make a transaction to apply for this role.
-          </p>
+      </p>
+      <p>
+        Before the transaction, a new account key, called a <em>role key</em>, will be generated and downloaded automatically.
+        You probably won't need to use the role key directly, but you will need it in order to perform any duties in the role, so be sure to keep a backup.
+      </p>
       <ModalAccordion title="Transaction details">
         <Table basic='very'>
           <Table.Body>
@@ -1026,28 +1030,26 @@ directly.
       <h4>Your new role key</h4>
       <p>
         This role requires a new sub-key to be associated with your account.
-        You'll never have to use the key directly, but you will need it in order
+        You'll probably won't use the key directly, but you will need it in order
         to perform any duties in the role.
 	  </p>
       <p>
-        We've generated a new role key, <strong>{props.roleKeyName}</strong>, automatically. You can
-              download its backup file using the button below, or from the <Link to="#accounts">My account</Link>
-        &nbsp; section.
+        We've generated a new role key, <strong>{props.roleKeyName}</strong>, automatically.
+        A copy of the backup file should have been downloaded, or you can
+        get a backup from the <Link to="/accounts">My account</Link> section.
 	  </p>
-      <Message warning>
+      <Message warning icon>
+        <Icon name='warning sign' />
         <strong>Please make sure to save this file in a secure location as it is the only
-              way to restore your role key!</strong>
+          way to restore your role key!</strong>
+      </Message>
+      <Message warning icon>
+        <Icon name='unlock' />
+        <strong>
+          This role key has been generated with no password!
+          We strongly recommend that you set a password for it in the <Link to="/accounts">My account</Link> section.
+        </strong>
       </Message>
-      <Container className="cta">
-        <Button content='Download role key backup' icon='download' labelPosition='left' primary />
-        <Button
-          content='Go to My Roles'
-          icon='right arrow'
-          labelPosition='right'
-          color='teal'
-        />
-      </Container>
-
     </Container>
   )
 }
@@ -1078,6 +1080,10 @@ export type FlowModalProps = ConfirmStakesStageProps & FundSourceSelectorProps &
   setTxKeyAddress: (v: AccountId) => void
   activeStep: ProgressSteps
   setActiveStep: (v: ProgressSteps) => void
+  txInProgress: boolean
+  setTxInProgress: (v: boolean) => void
+  complete: boolean
+  setComplete: (v: boolean) => void
 }
 
 export const FlowModal = Loadable<FlowModalProps>(
@@ -1095,6 +1101,8 @@ export const FlowModal = Loadable<FlowModalProps>(
       appDetails, setAppDetails,
       txKeyAddress, setTxKeyAddress,
       activeStep, setActiveStep,
+      txInProgress, setTxInProgress,
+      complete, setComplete,
     } = props
 
     const accCtx = useMyAccount()
@@ -1102,8 +1110,6 @@ export const FlowModal = Loadable<FlowModalProps>(
       setTxKeyAddress(new AccountId(accCtx.state.address))
     }
 
-    const [complete, setComplete] = useState(false)
-
     const history = useHistory()
     const cancel = () => {
       if (history.length > 1) {
@@ -1142,11 +1148,14 @@ export const FlowModal = Loadable<FlowModalProps>(
 
     const enterDoneState = () => {
       scrollToTop()
+      setTxInProgress(true)
       props.makeApplicationTransaction().then(() => {
         setComplete(true)
+        setTxInProgress(false)
         setActiveStep(ProgressSteps.Done)
       })
         .catch((e) => {
+          setTxInProgress(false)
           console.error("makeApplicationTransaction", e)
         })
     }
@@ -1221,6 +1230,11 @@ export const FlowModal = Loadable<FlowModalProps>(
             </Grid.Column>
           </Grid>
         </Container>
+        {txInProgress &&
+          <div className="loading">
+            <div className="spinner"></div>
+          </div>
+        }
       </Container>
     )
   })

+ 10 - 0
packages/joy-roles/src/transport.mock.ts

@@ -489,5 +489,15 @@ export class Transport extends TransportBase implements ITransport {
     }
     )
   }
+
+  async applyToCuratorOpening(
+    id: number,
+    roleAccountName: string,
+    sourceAccount: string,
+    appStake: Balance,
+    roleStake: Balance,
+    applicationText: string): Promise<number> {
+      return 0
+  }
 }
 

+ 34 - 9
packages/joy-roles/src/transport.polkadot.ts

@@ -20,7 +20,10 @@ import { Curator, CuratorId, CuratorApplication, CuratorInduction, CuratorRoleSt
 import { Application, Opening, OpeningId } from '@joystream/types/hiring';
 import { Stake, StakeId } from '@joystream/types/stake';
 import { Recipient, RewardRelationship, RewardRelationshipId } from '@joystream/types/recurring-rewards';
-import { Profile } from '@joystream/types/members';
+import { Profile, MemberId } from '@joystream/types/members';
+
+// FIXME: Move these functions to a dedicayed utils package
+import { createAccount, generateSeed } from '@polkadot/joy-utils/accounts'
 
 import { WorkingGroupMembership, StorageAndDistributionMembership } from "./tabs/WorkingGroup"
 import { WorkingGroupOpening } from "./tabs/Opportunities"
@@ -384,19 +387,42 @@ export class Transport extends TransportBase implements ITransport {
     )
   }
 
+  protected generateRoleAccount(name: string, password: string = ''): string | null {
+    const { address, deriveError, derivePath, isSeedValid, pairType, seed } = generateSeed(null, '', 'bip')
+
+    const isValid = !!address && !deriveError && isSeedValid;
+    if (!isValid) {
+      return null
+    }
+
+    const status = createAccount(`${seed}${derivePath}`, pairType, name, password, 'created account');
+    return status.account as string
+  }
+
   async applyToCuratorOpening(
     id: number,
-    memberId: number,
-    roleAccount: string,
+    roleAccountName: string,
     sourceAccount: string,
     appStake: Balance,
     roleStake: Balance,
     applicationText: string): Promise<number> {
-    return new Promise<number>((resolve, reject) => {
+    return new Promise<number>(async (resolve, reject) => {
+      const membershipIds = (
+        await this.api.query.members.memberIdsByControllerAccountId(sourceAccount)
+      ) as Vec<MemberId>
+      if (membershipIds.length == 0) {
+        reject("No membship ID associated with this address")
+      }
+
+      const roleAccount = this.generateRoleAccount(roleAccountName)
+      if (!roleAccount) {
+        reject('failed to create role account')
+      }
+
       const tx = this.api.tx.contentWorkingGroup.applyOnCuratorOpening(
-        memberId,
+        membershipIds[0],
         new u32(id),
-        new GenericAccountId(roleAccount),
+        new GenericAccountId(roleAccount as string),
         roleStake,
         appStake,
         new Text(applicationText),
@@ -417,8 +443,7 @@ export class Transport extends TransportBase implements ITransport {
         txFailedCb,
         txSuccessCb,
       });
-
-      // TODO: On success, the form state must be flushed
-    })
+    }
+    )
   }
 }

+ 1 - 2
packages/joy-roles/src/transport.ts

@@ -24,8 +24,7 @@ export interface ITransport {
   myCurationGroupRoles: () => Subscribable<ActiveRole[]>
   myStorageGroupRoles: () => Subscribable<ActiveRole[]>
   applyToCuratorOpening: (id: number,
-    memberId: number,
-    roleAccount: string,
+    roleAccountName: string,
     sourceAccount: string,
     appStake: Balance,
     roleStake: Balance,

+ 137 - 0
packages/joy-utils/src/accounts.ts

@@ -0,0 +1,137 @@
+import { ActionStatus } from '@polkadot/react-components/Status/types';
+import { CreateResult } from '@polkadot/ui-keyring/types';
+import { KeypairType } from '@polkadot/util-crypto/types';
+
+import FileSaver from 'file-saver';
+import { DEV_PHRASE } from '@polkadot/keyring/defaults';
+import { InputAddress } from '@polkadot/react-components';
+import keyring from '@polkadot/ui-keyring';
+import { isHex, u8aToHex } from '@polkadot/util';
+import { keyExtractSuri, mnemonicGenerate, mnemonicValidate, randomAsU8a } from '@polkadot/util-crypto';
+
+export type SeedType = 'bip' | 'raw' | 'dev';
+
+export interface AddressState {
+  address: string | null;
+  deriveError: string | null;
+  derivePath: string;
+  isSeedValid: boolean;
+  pairType: KeypairType;
+  seed: string;
+  seedType: SeedType;
+}
+
+const DEFAULT_PAIR_TYPE = 'ed25519'; // 'sr25519';
+
+function deriveValidate (seed: string, derivePath: string, pairType: KeypairType): string | null {
+  try {
+    const { path } = keyExtractSuri(`${seed}${derivePath}`);
+
+    // we don't allow soft for ed25519
+    if (pairType === 'ed25519' && path.some(({ isSoft }): boolean => isSoft)) {
+      return 'Soft derivation paths are not allowed on ed25519';
+    }
+  } catch (error) {
+    return error.message;
+  }
+
+  return null;
+}
+
+function isHexSeed (seed: string): boolean {
+  return isHex(seed) && seed.length === 66;
+}
+
+function rawValidate (seed: string): boolean {
+  return ((seed.length > 0) && (seed.length <= 32)) || isHexSeed(seed);
+}
+
+function addressFromSeed (phrase: string, derivePath: string, pairType: KeypairType): string {
+  return keyring
+    .createFromUri(`${phrase.trim()}${derivePath}`, {}, pairType)
+    .address;
+}
+
+function newSeed (seed: string | undefined | null, seedType: SeedType): string {
+  switch (seedType) {
+    case 'bip':
+      return mnemonicGenerate();
+    case 'dev':
+      return DEV_PHRASE;
+    default:
+      return seed || u8aToHex(randomAsU8a());
+  }
+}
+
+export function generateSeed (_seed: string | undefined | null, derivePath: string, seedType: SeedType, pairType: KeypairType = DEFAULT_PAIR_TYPE): AddressState {
+  const seed = newSeed(_seed, seedType);
+  const address = addressFromSeed(seed, derivePath, pairType);
+
+  return {
+    address,
+    deriveError: null,
+    derivePath,
+    isSeedValid: true,
+    pairType,
+    seedType,
+    seed
+  };
+}
+
+export function updateAddress (seed: string, derivePath: string, seedType: SeedType, pairType: KeypairType): AddressState {
+  const deriveError = deriveValidate(seed, derivePath, pairType);
+  let isSeedValid = seedType === 'raw'
+    ? rawValidate(seed)
+    : mnemonicValidate(seed);
+  let address: string | null = null;
+
+  if (!deriveError && isSeedValid) {
+    try {
+      address = addressFromSeed(seed, derivePath, pairType);
+    } catch (error) {
+      isSeedValid = false;
+    }
+  }
+
+  return {
+    address,
+    deriveError,
+    derivePath,
+    isSeedValid,
+    pairType,
+    seedType,
+    seed
+  };
+}
+
+export function downloadAccount ({ json, pair }: CreateResult): void {
+  const blob = new Blob([JSON.stringify(json)], { type: 'application/json; charset=utf-8' });
+
+  FileSaver.saveAs(blob, `${pair.address}.json`);
+  InputAddress.setLastValue('account', pair.address);
+}
+
+export function createAccount (suri: string, pairType: KeypairType, name: string, password: string, success: string): ActionStatus {
+  // we will fill in all the details below
+  const status = { action: 'create' } as ActionStatus;
+
+  try {
+    const result = keyring.addUri(suri, password, { name: name.trim(), tags: [] }, pairType);
+    const { address } = result.pair;
+
+    status.account = address;
+    status.status = 'success';
+    status.message = success;
+
+    downloadAccount(result);
+  } catch (error) {
+    status.status = 'error';
+    status.message = error.message;
+  }
+
+  return status;
+}
+
+export function isPasswordValid(password: string): boolean {
+  return password.length == 0 || keyring.isPassValid(password)
+}