Skip to content

Commit

Permalink
feat: persist keys on disk, closes #145
Browse files Browse the repository at this point in the history
feat: add test set up
  • Loading branch information
kyranjamie committed Jun 25, 2020
1 parent 12bd3bf commit c075d11
Show file tree
Hide file tree
Showing 39 changed files with 1,752 additions and 1,004 deletions.
26 changes: 0 additions & 26 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,35 +1,9 @@
# Logs
logs
*.log

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
.eslintcache

# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules

# OSX
.DS_Store

# App packaged
release
app/main.prod.js
Expand Down
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
root: true,
extends: ['@blockstack/eslint-config', 'plugin:react-hooks/recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
Expand All @@ -11,6 +12,8 @@ module.exports = {
rules: {
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-unsafe-member-access': 0,
'@typescript-eslint/no-unsafe-assignment': 0,
'@typescript-eslint/no-unsafe-call': 0,
'no-warning-comments': ['warn', { terms: ['SECURITY'], location: 'anywhere' }],
},
};
32 changes: 32 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Testing

on:
push:
branches: ['release/stacking']
pull_request:
branches: ['release/stacking']

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Use Node.js
uses: actions/setup-node@v1

- name: Cache node_modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ hashFiles('**/yarn.lock') }}

- name: Install dependencies
run: yarn

- name: Build prod
run: yarn build

- name: Run jest
run: yarn test
1 change: 1 addition & 0 deletions app/constants/routes.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"HOME": "/",
"SIGN_IN": "/sign-in",
"WELCOME": "/onboard/welcome",
"CREATE": "/onboard/create",
"RESTORE": "/onboard/restore",
Expand Down
5 changes: 4 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
"postinstall": "yarn electron-rebuild"
},
"license": "MIT",
"dependencies": {}
"dependencies": {
"@blockstack/keychain": "^0.5.0",
"blockstack": "21.1.0"
}
}
10 changes: 0 additions & 10 deletions app/pages/app.tsx

This file was deleted.

26 changes: 18 additions & 8 deletions app/pages/home/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ 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';
import { selectMnemonic, selectKeysSlice } from '../../store/keys';
import { BIP32Interface } from '../../types';

//
// Placeholder component
export const Home: React.FC = () => {
const mnemonic = useSelector(selectMnemonic);
const keys = useSelector(selectKeysSlice);
const [keychain, setKeychain] = useState<{ rootNode: BIP32Interface } | null>(null);

useEffect(() => {
const deriveMasterKeychain = async () => {
if (!mnemonic) return;
Expand All @@ -21,19 +23,27 @@ export const Home: React.FC = () => {
void deriveMasterKeychain();
}, [mnemonic]);

const { privateKey } = keychain?.rootNode
? deriveStxAddressChain(ChainID.Testnet)(keychain.rootNode)
: { privateKey: '' };
if (keychain === null) return <div>Homepage, but no keychain can be derived</div>;

const rootNode = deriveStxAddressChain(ChainID.Testnet)(keychain.rootNode);

const privateKey = rootNode.privateKey;

const base58 = rootNode.childKey.toBase58();

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

console.log(keychain);
// 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">MnemonicEncryptedHex: {privateKey}</Box>
<Box mt="loose">Private key: {privateKey}</Box>
<Box mt="loose">Base58: {base58}</Box>
<Box mt="loose">Salt: {(keys as any).salt}</Box>
<Box mt="loose">Password: {(keys as any).password}</Box>
<Box mt="loose">Stretched Key: {(keys as any).derivedEncryptionKey}</Box>
</Flex>
);
};
10 changes: 7 additions & 3 deletions app/pages/onboarding/08-set-password/set-password.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router';
import { Input } from '@blockstack/ui';

import routes from '../../../constants/routes.json';
import { setPassword as setPasswordAction } from '../../../store/keys';
import { Onboarding, OnboardingTitle, OnboardingButton } from '../../../components/onboarding';
import { ErrorLabel } from '../../../components/error-label';
import { ErrorText } from '../../../components/error-text';
Expand All @@ -27,9 +28,11 @@ const weakPasswordWarningMessage = (result: ValidatedPassword) => {

export const SetPassword: React.FC = () => {
const history = useHistory();
const dispatch = useDispatch();
const [password, setPassword] = useState<string | null>(null);
const [strengthResult, setStrengthResult] = useState(blankPasswordValidation);
const [hasSubmitted, setHasSubmitted] = useState(false);
const [btnDisabled, setBtnDisabled] = useState(false);

const handlePasswordInput = (e: React.FormEvent<HTMLInputElement>) => {
e.preventDefault();
Expand All @@ -44,7 +47,8 @@ export const SetPassword: React.FC = () => {
const result = validatePassword(password);
setStrengthResult(result);
if (result.meetsAllStrengthRequirements) {
history.push(routes.HOME);
setBtnDisabled(true);
dispatch(setPasswordAction({ password, history }));
}
};

Expand All @@ -57,7 +61,7 @@ export const SetPassword: React.FC = () => {
<ErrorText>{weakPasswordWarningMessage(strengthResult)}</ErrorText>
</ErrorLabel>
)}
<OnboardingButton type="submit" mt="loose">
<OnboardingButton type="submit" mt="loose" isDisabled={btnDisabled}>
Continue
</OnboardingButton>
</Onboarding>
Expand Down
6 changes: 3 additions & 3 deletions app/pages/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Link } from 'react-router-dom';
import { hot } from 'react-hot-loader/root';
import { History } from 'history';

import { Store } from '../store/reducers';
import { Store } from '../store';
import { Routes, routerConfig } from '../routes';
import { loadFonts } from '../utils/load-fonts';

Expand All @@ -17,7 +17,7 @@ const GlobalStyle = createGlobalStyle`
}
`;

function DevFooter() {
export function DevFooter() {
return (
<Box position="absolute" top="base" left="loose">
{routerConfig.map((route, i) => (
Expand All @@ -40,12 +40,12 @@ function Root({ store, history }: RootProps) {
useEffect(() => {
void loadFonts();
}, []);

return (
<Provider store={store}>
<CSSReset />
<GlobalStyle />
<ConnectedRouter history={history}>
{/* <DevFooter /> */}
<Routes />
</ConnectedRouter>
</Provider>
Expand Down
45 changes: 45 additions & 0 deletions app/pages/sign-in/sign-in.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router';
import { Input } from '@blockstack/ui';

import { Onboarding, OnboardingTitle, OnboardingButton } from '../../components/onboarding';
import { ErrorLabel } from '../../components/error-label';
import { ErrorText } from '../../components/error-text';
import { decryptWallet, selectKeysSlice } from '../../store/keys';

export const SignIn: React.FC = () => {
const history = useHistory();
const dispatch = useDispatch();
const [password, setPassword] = useState<string | null>(null);
const [hasSubmitted, setHasSubmitted] = useState(false);
const keysState = useSelector(selectKeysSlice);

const handlePasswordInput = (e: React.FormEvent<HTMLInputElement>) => {
e.preventDefault();
const pass = e.currentTarget.value;
setPassword(pass);
};

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setHasSubmitted(true);
if (!password) return;
dispatch(decryptWallet({ password, history }));
};

return (
<Onboarding as="form" onSubmit={handleSubmit}>
<OnboardingTitle>Enter your password</OnboardingTitle>
<Input type="password" mt="extra-loose" onChange={handlePasswordInput} />
{hasSubmitted && keysState.decryptionError && (
<ErrorLabel>
<ErrorText>Password entered is incorrect</ErrorText>
</ErrorLabel>
)}
<OnboardingButton type="submit" mt="loose" isLoading={keysState.decrypting}>
Continue
</OnboardingButton>
</Onboarding>
);
};
19 changes: 15 additions & 4 deletions app/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from 'react';
import { useStore } from 'react-redux';
import { Switch, Route, Redirect } from 'react-router-dom';

import routes from './constants/routes.json';
import App from './pages/app';
import { Home } from './pages/home/home';
import { selectKeysSlice } from './store/keys/keys.reducer';
import { SignIn } from './pages/sign-in/sign-in';
import {
Welcome,
CreateWallet,
Expand All @@ -20,6 +22,10 @@ export const routerConfig = [
path: routes.HOME,
component: Home,
},
{
path: routes.SIGN_IN,
component: SignIn,
},
{
path: routes.WELCOME,
component: Welcome,
Expand Down Expand Up @@ -58,15 +64,20 @@ export const routerConfig = [
},
];

const getAppStartingRoute = (salt?: string) => (!!salt ? routes.SIGN_IN : routes.WELCOME);

export function Routes() {
// `useStore` required as we only want the value on initial render
const store = useStore();
const { salt } = selectKeysSlice(store.getState());
return (
<App>
<>
<Switch>
{routerConfig.map((route, i) => (
<Route key={i} exact {...route} />
))}
</Switch>
<Redirect exact from="/" to={routes.WELCOME} />
</App>
<Redirect exact from="/" to={getAppStartingRoute(salt)} />
</>
);
}
7 changes: 4 additions & 3 deletions app/store/configureStore.dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import thunk from 'redux-thunk';
import { createHashHistory } from 'history';
import { routerMiddleware, routerActions } from 'connected-react-router';
import { createLogger } from 'redux-logger';
import { RootState, createRootReducer } from './reducers';
import { RootState, createRootReducer } from '.';
import { getInitialStateFromDisk } from '../utils/disk-store';

declare global {
interface Window {
Expand All @@ -20,7 +21,7 @@ declare global {

export const history = createHashHistory();

const rootReducer = createRootReducer(history);
const rootReducer = createRootReducer({ history, keys: getInitialStateFromDisk() });

export const configureStore = (initialState?: RootState) => {
// Redux Configuration
Expand Down Expand Up @@ -66,7 +67,7 @@ export const configureStore = (initialState?: RootState) => {
const store = createStore(rootReducer, initialState, enhancer);

if (module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(require('./reducers').default));
module.hot.accept('./index', () => store.replaceReducer(require('./index').default));
}

return store;
Expand Down
5 changes: 3 additions & 2 deletions app/store/configureStore.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { createStore, applyMiddleware } from '@reduxjs/toolkit';
import thunk from 'redux-thunk';
import { createHashHistory } from 'history';
import { routerMiddleware } from 'connected-react-router';
import { RootState, createRootReducer } from './reducers';
import { RootState, createRootReducer } from '.';
import { getInitialStateFromDisk } from '../utils/disk-store';

export const history = createHashHistory();
const rootReducer = createRootReducer(history);
const rootReducer = createRootReducer({ history, keys: getInitialStateFromDisk() });
const router = routerMiddleware(history);
const enhancer = applyMiddleware(thunk, router);

Expand Down
Loading

0 comments on commit c075d11

Please sign in to comment.