ghost-portal/packages/page-accounts/src/Accounts/modals/Create.tsx
Jaco Greeff 964eb7f571
Add contacts to Accounts, all-in-one (#2453)
* Add contacts to Accounts, all-in-one

* Vanity generator i18n

* yarn
2020-03-24 13:37:11 +01:00

326 lines
11 KiB
TypeScript

// Copyright 2017-2020 @polkadot/app-accounts 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 { 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 FileSaver from 'file-saver';
import React, { useMemo, useState } from 'react';
import styled from 'styled-components';
import { DEV_PHRASE } from '@polkadot/keyring/defaults';
import { AddressRow, Button, Dropdown, Expander, Input, InputAddress, Modal, Password } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import keyring from '@polkadot/ui-keyring';
import uiSettings from '@polkadot/ui-settings';
import { isHex, u8aToHex } from '@polkadot/util';
import { keyExtractSuri, mnemonicGenerate, mnemonicValidate, randomAsU8a } from '@polkadot/util-crypto';
import { useTranslation } from '../../translate';
import CreateConfirmation from './CreateConfirmation';
interface Props extends ModalProps {
className?: string;
seed?: string;
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;
}
interface CreateOptions {
genesisHash?: string;
name: string;
tags?: string[];
}
const DEFAULT_PAIR_TYPE = '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, { genesisHash, name, tags = [] }: CreateOptions, 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, { genesisHash, name, 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, type: propsType }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api, isDevelopment } = useApi();
const [{ address, deriveError, derivePath, isSeedValid, pairType, seed, seedType }, setAddress] = useState<AddressState>(generateSeed(propsSeed, '', propsSeed ? 'raw' : 'bip', propsType));
const [isConfirmationOpen, setIsConfirmationOpen] = useState(false);
const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
const [{ isPassValid, password }, setPassword] = useState({ isPassValid: false, password: '' });
const [{ isPass2Valid, password2 }, setPassword2] = useState({ isPass2Valid: false, password2: '' });
const isValid = !!address && !deriveError && isNameValid && isPassValid && isPass2Valid && isSeedValid;
const seedOpt = useMemo(() => (
isDevelopment
? [{ value: 'dev', text: t('Development') }]
: []
).concat(
{ value: 'bip', text: t('Mnemonic') },
{ value: 'raw', text: t('Raw seed') }
), [isDevelopment, t]);
const _onChangePass = (password: string): void =>
setPassword({ isPassValid: keyring.isPassValid(password), password });
const _onChangePass2 = (password2: string): void =>
setPassword2({ isPass2Valid: keyring.isPassValid(password2) && (password2 === password), password2 });
const _onChangeDerive = (newDerivePath: string): void =>
setAddress(updateAddress(seed, newDerivePath, seedType, pairType));
const _onChangeSeed = (newSeed: string): void =>
setAddress(updateAddress(newSeed, derivePath, seedType, pairType));
const _onChangePairType = (newPairType: KeypairType): void =>
setAddress(updateAddress(seed, derivePath, seedType, newPairType));
const _selectSeedType = (newSeedType: SeedType): void => {
if (newSeedType !== seedType) {
setAddress(generateSeed(null, derivePath, newSeedType, pairType));
}
};
const _onChangeName = (name: string): void => setName({ isNameValid: !!name.trim(), name });
const _toggleConfirmation = (): void => setIsConfirmationOpen(!isConfirmationOpen);
const _onCommit = (): void => {
if (!isValid) {
return;
}
const options = { genesisHash: isDevelopment ? undefined : api.genesisHash.toString(), name: name.trim() };
const status = createAccount(`${seed}${derivePath}`, pairType, options, password, t('created account'));
_toggleConfirmation();
onStatusChange(status);
onClose();
};
return (
<Modal
className={className}
header={t('Add an account via seed')}
>
{address && isConfirmationOpen && (
<CreateConfirmation
address={address}
name={name}
onCommit={_onCommit}
onClose={_toggleConfirmation}
/>
)}
<Modal.Content>
<AddressRow
defaultName={name}
noDefaultNameOpacity
value={isSeedValid ? address : ''}
>
<Input
autoFocus
className='full'
help={t('Name given to this account. You can edit it. To use the account to validate or nominate, it is a good practice to append the function of the account in the name, e.g "name_you_want - stash".')}
isError={!isNameValid}
label={t('name')}
onChange={_onChangeName}
onEnter={_onCommit}
placeholder={t('new account')}
value={name}
/>
<Input
className='full'
help={t('The private key for your account is derived from this seed. This seed must be kept secret as anyone in its possession has access to the funds of this account. If you validate, use the seed of the session account as the "--key" parameter of your node.')}
isAction
isError={!isSeedValid}
isReadOnly={seedType === 'dev'}
label={
seedType === 'bip'
? t('mnemonic seed')
: seedType === 'dev'
? t('development seed')
: t('seed (hex or string)')
}
onChange={_onChangeSeed}
onEnter={_onCommit}
value={seed}
>
<Dropdown
isButton
defaultValue={seedType}
onChange={_selectSeedType}
options={seedOpt}
/>
</Input>
<Password
className='full'
help={t('This password is used to encrypt your private key. It must be strong and unique! You will need it to sign transactions with this account. You can recover this account using this password together with the backup file (generated in the next step).')}
isError={!isPassValid}
label={t('password')}
onChange={_onChangePass}
onEnter={_onCommit}
value={password}
/>
<Password
className='full'
help={t('Verify the password entered above.')}
isError={!isPass2Valid}
label={t('password (repeat)')}
onChange={_onChangePass2}
onEnter={_onCommit}
value={password2}
/>
<Expander
className='accounts--Creator-advanced'
isOpen
summary={t('Advanced creation options')}
>
<Dropdown
defaultValue={pairType}
help={t('Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use "ed25519".')}
label={t('keypair crypto type')}
onChange={_onChangePairType}
options={uiSettings.availableCryptos}
/>
<Input
className='full'
help={t('You can set a custom derivation path for this account using the following syntax "/<soft-key>//<hard-key>///<password>". The "/<soft-key>" and "//<hard-key>" may be repeated and mixed`. The "///password" is optional and should only occur once.')}
isError={!!deriveError}
label={t('secret derivation path')}
onChange={_onChangeDerive}
onEnter={_onCommit}
placeholder={t('//hard/soft///password')}
value={derivePath}
/>
{deriveError && (
<article className='error'>{deriveError}</article>
)}
</Expander>
</AddressRow>
</Modal.Content>
<Modal.Actions onCancel={onClose}>
<Button
icon='plus'
isDisabled={!isValid}
isPrimary
label={t('Save')}
onClick={_toggleConfirmation}
/>
</Modal.Actions>
</Modal>
);
}
export default React.memo(styled(Create)`
.accounts--Creator-advanced {
margin-top: 1rem;
}
`);