Skip to content

Commit

Permalink
feat: add mnemonic phrase generation, closes #142, #143, #138
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Jun 17, 2020
1 parent 0ad117b commit 74fb858
Show file tree
Hide file tree
Showing 31 changed files with 1,119 additions and 629 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ app/main.prod.js
app/main.prod.js.map
app/renderer.prod.js
app/renderer.prod.js.map
app/node_modules
app/style.css
app/style.css.map
dist
Expand Down
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = {
},
rules: {
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-unsafe-member-access': 0,
'no-warning-comments': ['warn', { terms: ['SECURITY'], location: 'anywhere' }],
},
};
5 changes: 3 additions & 2 deletions app/components/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const Card: React.FC<CardProps> = ({ title, children, ...rest }) => {
borderColor="#E5E5EC"
boxShadow="mid"
textAlign="center"
width="100%"
{...rest}
>
<Flex
Expand All @@ -26,8 +27,8 @@ export const Card: React.FC<CardProps> = ({ title, children, ...rest }) => {
{title}
</Text>
</Flex>
<Box px={10} py={5}>
<Text>{children}</Text>
<Box my="base" mx="base">
{children}
</Box>
</Box>
);
Expand Down
11 changes: 0 additions & 11 deletions app/components/seed-textarea.tsx

This file was deleted.

1 change: 1 addition & 0 deletions app/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MNEMONIC_ENTROPY = 256;
1 change: 1 addition & 0 deletions app/constants/routes.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"HOME": "/",
"WELCOME": "/onboard/welcome",
"CREATE": "/onboard/create",
"RESTORE": "/onboard/restore",
Expand Down
1 change: 0 additions & 1 deletion app/main.dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ const installExtensions = async () => {
const installer = require('electron-devtools-installer');
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];

return Promise.all(
extensions.map(name => installer.default(installer[name], forceDownload))
).catch(console.log);
Expand Down
39 changes: 39 additions & 0 deletions app/pages/home/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { Flex, Box } from '@blockstack/ui';
import { ChainID } from '@blockstack/stacks-transactions';
import { deriveRootKeychainFromMnemonic, deriveStxAddressChain } from '@blockstack/keychain';

import { selectMnemonic } from '../../store/keys';
import { BIP32Interface } from '@blockstack/keychain/node_modules/bip32';

//
// Placeholder component
export const Home: React.FC = () => {
const mnemonic = useSelector(selectMnemonic);
const [keychain, setKeychain] = useState<{ rootNode: BIP32Interface } | null>(null);
useEffect(() => {
const deriveMasterKeychain = async () => {
if (!mnemonic) return;
const resp = await deriveRootKeychainFromMnemonic(mnemonic, '');
setKeychain(resp);
};
void deriveMasterKeychain();
}, [mnemonic]);

const { privateKey } = keychain?.rootNode
? deriveStxAddressChain(ChainID.Testnet)(keychain.rootNode)
: { privateKey: '' };

if (!mnemonic) return <>'How you get to homepage without a mnemonic?'</>;

console.log(keychain);

return (
<Flex pt="120px" flexDirection="column" mx="loose">
<Box>Mnemonic: {mnemonic}</Box>
<Box mt="loose">MnemonicEncryptedHex: {(keychain as any)?.encryptedMnemonicHex}</Box>
<Box mt="loose">Private key: {privateKey}</Box>
</Flex>
);
};
17 changes: 8 additions & 9 deletions app/pages/onboarding/01-welcome/welcome.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { useHistory } from 'react-router-dom';

import routes from '../../../constants/routes.json';
import {
Expand All @@ -10,18 +10,17 @@ import {
} from '../../../components/onboarding';

export const Welcome: React.FC = () => {
const history = useHistory();
return (
<Onboarding>
<OnboardingTitle>Stacks Wallet</OnboardingTitle>
<OnboardingText>Send, receive, and and earn Bitcoin rewards</OnboardingText>
<Link to={routes.CREATE}>
<OnboardingButton mt="extra-loose">Create a new wallet</OnboardingButton>
</Link>
<Link to={routes.RESTORE}>
<OnboardingButton mt="base" variant="outline">
I already have a wallet
</OnboardingButton>
</Link>
<OnboardingButton mt="extra-loose" onClick={() => history.push(routes.CREATE)}>
Create a new wallet
</OnboardingButton>
<OnboardingButton onClick={() => history.push(routes.RESTORE)} mt="base" mode="alternate">
I already have a wallet
</OnboardingButton>
</Onboarding>
);
};
20 changes: 16 additions & 4 deletions app/pages/onboarding/02-create-wallet/create-wallet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';

import { onboardingMnemonicGenerationStep } from '../../../store/keys/keys.actions';
import routes from '../../../constants/routes.json';
import {
Onboarding,
Expand All @@ -12,6 +14,14 @@ import {
} from '../../../components/onboarding';

export const CreateWallet: React.FC = () => {
const dispatch = useDispatch();
const history = useHistory();

const createSoftwareWallet = () => {
dispatch(onboardingMnemonicGenerationStep({ stepDelayMs: 1_500 }));
history.push(routes.GENERATING);
};

return (
<Onboarding>
<OnboardingTitle>Create a new wallet</OnboardingTitle>
Expand All @@ -20,9 +30,11 @@ export const CreateWallet: React.FC = () => {
wallet
</OnboardingText>
<OnboardingButton mt="extra-loose">Use a Ledger wallet</OnboardingButton>
<Link to={routes.GENERATING}>
<OnboardingButton>Create a software wallet</OnboardingButton>
</Link>

<OnboardingButton onClick={createSoftwareWallet} mode="alternate">
Create a software wallet
</OnboardingButton>

<OnboardingFooter>
<OnboardingFooterLink>I have a Trezor wallet</OnboardingFooterLink>
</OnboardingFooter>
Expand Down
50 changes: 46 additions & 4 deletions app/pages/onboarding/03-restore-wallet/restore-wallet.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import React from 'react';
import React, { useState } from 'react';
import { useHistory } from 'react-router';
import { useDispatch } from 'react-redux';
import { deriveRootKeychainFromMnemonic } from '@blockstack/keychain';
import { Text, Input } from '@blockstack/ui';

import routes from '../../../constants/routes.json';
import { Hr } from '../../../components/hr';
import { ErrorLabel } from '../../../components/error-label';
import { ErrorText } from '../../../components/error-text';
import { persistMnemonic } from '../../../store/keys/keys.actions';
import { safeAwait } from '../../../utils/safe-await';
import {
Onboarding,
OnboardingTitle,
Expand All @@ -12,24 +20,58 @@ import {
} from '../../../components/onboarding';

export const RestoreWallet: React.FC = () => {
const [mnemonic, setMnemonic] = useState('');
const [error, setError] = useState<string | null>(null);
const history = useHistory();
const dispatch = useDispatch();

const handleMnemonicInput = (e: React.FormEvent<HTMLInputElement>) => {
setMnemonic(e.currentTarget.value.trim());
};

const handleSecretKeyRestore = async (e: React.FormEvent) => {
e.preventDefault();
if (mnemonic.split(' ').length !== 24) {
setError('The Stacks Wallet can only be used with 24-word Secret Keys');
return;
}
const [error] = await safeAwait(deriveRootKeychainFromMnemonic(mnemonic, ''));
if (error) {
setError('Not a valid bip39 mnemonic');
return;
}
dispatch(persistMnemonic(mnemonic));
history.push(routes.SET_PASSWORD);
};

return (
<Onboarding>
<Onboarding as="form" onSubmit={handleSecretKeyRestore}>
<OnboardingTitle>Restore your wallet</OnboardingTitle>
<OnboardingText>
Restore your wallet by connecting your Ledger hardware wallet or a by entering your Secret
Key
</OnboardingText>
<OnboardingButton mt="extra-loose">Continue with Ledger</OnboardingButton>

<Hr my="extra-loose" />

<Text textStyle="body.small.medium">Secret Key</Text>
<Input
onChange={handleMnemonicInput}
as="textarea"
mt="base-tight"
height="88px"
minHeight="88px"
placeholder="24-word Secret Key"
style={{ resize: 'none' }}
/>
<OnboardingButton mt="loose">Continue with Secret Key</OnboardingButton>
{error && (
<ErrorLabel>
<ErrorText>{error}</ErrorText>
</ErrorLabel>
)}
<OnboardingButton mt="loose" type="submit" mode="alternate">
Continue with Secret Key
</OnboardingButton>
<OnboardingFooter>
<OnboardingFooterLink>I have a Trezor wallet</OnboardingFooterLink>
</OnboardingFooter>
Expand Down
12 changes: 1 addition & 11 deletions app/pages/onboarding/04-generating-secret/generating-secret.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import React from 'react';
import { Spinner } from '@blockstack/ui';

import routes from '../../../constants/routes.json';
import { Onboarding, OnboardingTitle } from '../../../components/onboarding';

const GENERATION_TIME = 2_500;

export const GeneratingSecret: React.FC = () => {
const history = useHistory();

useEffect(() => {
setTimeout(() => history.push(routes.SECRET_KEY), GENERATION_TIME);
}, [history]);

return (
<Onboarding pt="152px">
<Spinner size="lg" color="blue" alignSelf="center" />
Expand Down
35 changes: 19 additions & 16 deletions app/pages/onboarding/05-secret-key/secret-key.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
import { useClipboard } from '@blockstack/ui';
import { useHistory } from 'react-router-dom';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import log from 'electron-log';
import { useClipboard } from '@blockstack/ui';

import routes from '../../../constants/routes.json';
import { Card } from '../../../components/card';
import { Toast } from '../../../components/toast';
import { SeedTextarea } from '../../../components/seed-textarea';

import {
Onboarding,
OnboardingTitle,
OnboardingButton,
OnboardingText,
} from '../../../components/onboarding';
import { selectMnemonic } from '../../../store/keys/keys.reducer';

const COPY_TOAST_TIMEOUT = 3_000;
const COPY_TOAST_TIMEOUT = 2_500;

export const SecretKey: React.FC = () => {
const history = useHistory();
const [copied, setCopiedState] = useState(false);
const phrase =
'future act silly correct hold endorse essay save prefer filter donate clap future act silly correct hold endorse essay save prefer filter donate clap';
const { onCopy } = useClipboard(phrase);
const mnemonic = useSelector(selectMnemonic);

if (!mnemonic) {
const err = 'Component `SecretKey` should not render without pre-generated mnemonic';
log.error(err);
throw new Error(err);
}

const { onCopy } = useClipboard(mnemonic);
const copyToClipboard = () => {
onCopy && onCopy();
onCopy?.();
setCopiedState(true);
setTimeout(() => history.push(routes.SAVE_KEY), COPY_TOAST_TIMEOUT);
};

return (
<Onboarding>
<OnboardingTitle>Your Secret Key</OnboardingTitle>
Expand All @@ -34,14 +44,7 @@ export const SecretKey: React.FC = () => {
Once lost, it’s lost forever, so save it somewhere you won’t forget.
</OnboardingText>
<Card title="Your Secret Key" mt="extra-loose">
<SeedTextarea
readOnly
spellCheck="false"
autoCapitalize="false"
value={phrase}
className="hidden-secret-key"
data-test="textarea-seed-phrase"
/>
{mnemonic}
</Card>
<OnboardingButton mt="loose" onClick={() => copyToClipboard()}>
Copy Secret Key
Expand Down
11 changes: 7 additions & 4 deletions app/pages/onboarding/06-save-key/save-key.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { useHistory } from 'react-router-dom';

import routes from '../../../constants/routes.json';
import {
Expand All @@ -11,16 +11,19 @@ import {
import { Collapse, onboardingFaq } from '../../../components/secret-key-faq';

export const SaveKey: React.FC = () => {
const history = useHistory();
return (
<Onboarding>
<OnboardingTitle>Save your Secret Key</OnboardingTitle>
<OnboardingText>
Paste your Secret Key wherever you keep critical, private, information such as passwords.
Once lost, it’s lost forever. So save it somewhere you won’t forget.
</OnboardingText>
<Link to={routes.VERIFY_KEY}>
<OnboardingButton mt="extra-loose">I've saved it</OnboardingButton>
</Link>

<OnboardingButton mt="extra-loose" onClick={() => history.push(routes.VERIFY_KEY)}>
I've saved it
</OnboardingButton>

<Collapse content={onboardingFaq('Stacks Wallet')} mt="extra-loose" />
</Onboarding>
);
Expand Down
Loading

0 comments on commit 74fb858

Please sign in to comment.