= ({
+ balanceCard,
+ transactionList,
+ stackingPromoCard,
+ stackingRewardCard,
+}) => {
+ return (
+
+ {balanceCard}
+
+
+ {transactionList}
+
+
+ {stackingPromoCard}
+ {stackingRewardCard}
+
+
+ {/* */}
+ {/* Mnemonic: {mnemonic}
+ MnemonicEncryptedHex: {privateKey}
+ Private key: {privateKey}
+ Base58: {base58}
+ Salt: {(keys as any).salt}
+ Password: {(keys as any).password}
+ Stretched Key: {(keys as any).derivedEncryptionKey} */}
+
+ );
+};
diff --git a/app/pages/home/home.tsx b/app/pages/home/home.tsx
index 1c4a267..5797e4e 100644
--- a/app/pages/home/home.tsx
+++ b/app/pages/home/home.tsx
@@ -1,49 +1,40 @@
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 { deriveRootKeychainFromMnemonic } from '@blockstack/keychain';
-import { selectMnemonic, selectKeysSlice } from '../../store/keys';
+import { selectMnemonic } from '../../store/keys';
import { BIP32Interface } from '../../types';
+import { TransactionList } from '../../components/transaction-list/transaction-list';
+import { StackingPromoCard } from '../../components/stacking-promo-card';
+import { StackingRewardCard } from '../../components/stacking-rewards-card';
+import { BalanceCard } from '../../components/balance-card';
+import { HomeLayout } from './home-layout';
//
// 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;
- const resp = await deriveRootKeychainFromMnemonic(mnemonic, '');
- setKeychain(resp);
+ const { rootNode } = await deriveRootKeychainFromMnemonic(mnemonic, '');
+ setKeychain({ rootNode });
};
void deriveMasterKeychain();
}, [mnemonic]);
- if (keychain === null) return Homepage, but no keychain can be derived
;
-
- 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?>;
-
- // console.log(keychain);
+ if (keychain === null) return <>>;
return (
-
- Mnemonic: {mnemonic}
- MnemonicEncryptedHex: {privateKey}
- Private key: {privateKey}
- Base58: {base58}
- Salt: {(keys as any).salt}
- Password: {(keys as any).password}
- Stretched Key: {(keys as any).derivedEncryptionKey}
-
+ }
+ transactionList={}
+ stackingPromoCard={}
+ stackingRewardCard={
+
+ }
+ />
);
};
diff --git a/app/pages/root.tsx b/app/pages/root.tsx
index e39e907..e9c8e9a 100644
--- a/app/pages/root.tsx
+++ b/app/pages/root.tsx
@@ -13,7 +13,8 @@ import { loadFonts } from '../utils/load-fonts';
const GlobalStyle = createGlobalStyle`
html, body, #root {
- height: 100%;
+ min-height: 100vh;
+ max-height: 100vh;
}
`;
diff --git a/app/store/keys/keys.actions.ts b/app/store/keys/keys.actions.ts
index 8a24202..8894a4d 100644
--- a/app/store/keys/keys.actions.ts
+++ b/app/store/keys/keys.actions.ts
@@ -1,9 +1,14 @@
import { push } from 'connected-react-router';
import { createAction, Dispatch } from '@reduxjs/toolkit';
import { useHistory } from 'react-router';
+import log from 'electron-log';
import bcryptjs from 'bcryptjs';
import { memoizeWith, identity } from 'ramda';
-import { generateMnemonicRootKeychain } from '@blockstack/keychain';
+import {
+ generateMnemonicRootKeychain,
+ deriveRootKeychainFromMnemonic,
+ deriveStxAddressChain,
+} from '@blockstack/keychain';
import { encryptMnemonic, decryptMnemonic } from 'blockstack';
import routes from '../../constants/routes.json';
@@ -11,8 +16,8 @@ import { MNEMONIC_ENTROPY } from '../../constants';
import { RootState } from '../index';
import { persistSalt, persistEncryptedMnemonic } from '../../utils/disk-store';
import { safeAwait } from '../../utils/safe-await';
-import log from 'electron-log';
-import { selectMnemonic } from './keys.reducer';
+import { selectMnemonic, selectKeysSlice } from './keys.reducer';
+import { ChainID } from '@blockstack/stacks-transactions';
type History = ReturnType;
@@ -28,7 +33,7 @@ interface SetPasswordSuccess {
}
export const setPasswordSuccess = createAction('keys/set-password-success');
-export function onboardingMnemonicGenerationStep({ stepDelayMs }: { stepDelayMs: string }) {
+export function onboardingMnemonicGenerationStep({ stepDelayMs }: { stepDelayMs: number }) {
return async (dispatch: Dispatch) => {
const key = await generateMnemonicRootKeychain(MNEMONIC_ENTROPY);
dispatch(persistMnemonicSafe(key.plaintextMnemonic));
@@ -45,29 +50,30 @@ const generateSalt = memoizeWith(identity, async () => await bcryptjs.genSalt(12
export function setPassword({ password, history }: { password: string; history: History }) {
return async (dispatch: Dispatch, getState: () => RootState) => {
- const state = getState();
- const mnemonic = selectMnemonic(state);
+ const mnemonic = selectMnemonic(getState());
const salt = await generateSalt();
-
const derivedEncryptionKey = await generateDerivedKey({ password, salt });
+
if (!mnemonic) {
log.error('Cannot derive encryption key unless a mnemonic has been generated');
return;
}
+
const encryptedMnemonicBuffer = await encryptMnemonic(mnemonic, derivedEncryptionKey);
const encryptedMnemonic = encryptedMnemonicBuffer.toString('hex');
- // TEMP: to remove, useful for debugging
- dispatch(setPasswordSuccess({ salt, encryptedMnemonic }));
persistSalt(salt);
persistEncryptedMnemonic(encryptedMnemonic);
+ dispatch(setPasswordSuccess({ salt, encryptedMnemonic }));
history.push(routes.HOME);
};
}
export const attemptWalletDecrypt = createAction('keys/attempt-wallet-decrypt');
-export const attemptWalletDecryptSuccess = createAction<{ salt: string; mnemonic: string }>(
- 'keys/attempt-wallet-decrypt-success'
-);
+export const attemptWalletDecryptSuccess = createAction<{
+ salt: string;
+ mnemonic: string;
+ address: string;
+}>('keys/attempt-wallet-decrypt-success');
export const attemptWalletDecryptFailed = createAction<{ decryptionError: string }>(
'keys/attempt-wallet-decrypt-failed'
);
@@ -75,8 +81,13 @@ export const attemptWalletDecryptFailed = createAction<{ decryptionError: string
export function decryptWallet({ password, history }: { password: string; history: History }) {
return async (dispatch: Dispatch, getState: () => RootState) => {
dispatch(attemptWalletDecrypt());
- const salt = (getState().keys as any).salt as string;
- const encryptedMnemonic = (getState().keys as any).encryptedMnemonic as string;
+ const { salt, encryptedMnemonic } = selectKeysSlice(getState());
+
+ if (!salt || !encryptMnemonic) {
+ log.error('Cannot decrypt wallet if no `salt` or `encryptedMnemonic` exists');
+ return;
+ }
+
const key = await generateDerivedKey({ password, salt });
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -89,8 +100,13 @@ export function decryptWallet({ password, history }: { password: string; history
}
if (mnemonic) {
+ const { rootNode } = await deriveRootKeychainFromMnemonic(mnemonic, '');
+ console.log({ rootNode });
+ const { address } = deriveStxAddressChain(ChainID.Mainnet)(rootNode);
+ // const pubkey = pubKeyfromPrivKey(rootNode.privateKey);
+ // console.log({ addressKeychain });
+ dispatch(attemptWalletDecryptSuccess({ salt, mnemonic, address }));
history.push(routes.HOME);
- dispatch(attemptWalletDecryptSuccess({ salt, mnemonic }));
}
};
}
diff --git a/app/store/keys/keys.reducer.spec.ts b/app/store/keys/keys.reducer.spec.ts
index 827b68b..d71a952 100644
--- a/app/store/keys/keys.reducer.spec.ts
+++ b/app/store/keys/keys.reducer.spec.ts
@@ -33,13 +33,4 @@ describe('keysReducer', () => {
expect(result).toEqual({ mnemonic: 'test mnemonic' });
});
});
-
- describe('persistMnemonic', () => {
- test('it saves mnemonic, regardless', () => {
- const state = { mnemonic: 'twenty four words blah' } as KeysState;
- const action = persistMnemonic('test mnemonic');
- const result = reducer(state, action);
- expect(result).toEqual({ mnemonic: 'test mnemonic' });
- });
- });
});
diff --git a/app/store/keys/keys.reducer.ts b/app/store/keys/keys.reducer.ts
index 8cd26ad..d954307 100644
--- a/app/store/keys/keys.reducer.ts
+++ b/app/store/keys/keys.reducer.ts
@@ -15,7 +15,8 @@ export interface KeysState {
decrypting: boolean;
salt?: string;
decryptionError?: string;
- encryptMnemonic?: string;
+ encryptedMnemonic?: string;
+ address?: string;
}
const initialState: Readonly = Object.freeze({
@@ -46,7 +47,9 @@ export const createKeysReducer = (keys: Partial = {}) =>
.addCase(attemptWalletDecryptSuccess, (state, { payload }) => ({
...state,
salt: payload.salt,
+ decrypting: false,
mnemonic: payload.mnemonic,
+ address: payload.address,
}))
);
diff --git a/app/utils/safe-await.spec.ts b/app/utils/safe-await.spec.ts
new file mode 100644
index 0000000..802482d
--- /dev/null
+++ b/app/utils/safe-await.spec.ts
@@ -0,0 +1,131 @@
+import { safeAwait } from './safe-await';
+
+describe('safeAwait()', () => {
+ test('a valid promise has data', async () => {
+ const [err, data] = await safeAwait(new Promise(res => res({ objWithData: true })));
+ expect(err).toBeUndefined();
+ expect(data).toEqual({ objWithData: true });
+ });
+
+ test('error is returned', async () => {
+ const [err, data] = await safeAwait(new Promise((_res, reject) => reject({ error: true })));
+ expect(data).toBeUndefined();
+ expect(err).toEqual({ error: true });
+ });
+
+ test('throws on code syntax error "ReferenceError"', async () => {
+ expect.assertions(1);
+ try {
+ const [_err, _data] = await safeAwait(
+ new Promise(() => {
+ throw new ReferenceError('a native error');
+ })
+ );
+ } catch (error) {
+ expect(error).toEqual(new ReferenceError('a native error'));
+ }
+ });
+
+ test('returns error if it is valid response', async () => {
+ const [err, data] = await safeAwait(new Promise(res => res(new Error('test err'))));
+ expect(err).toEqual(new Error('test err'));
+ });
+
+ test('optional finally function is invoked', async () => {
+ const mockFn = jest.fn();
+ await safeAwait(new Promise(res => res({ objWithData: true })), mockFn);
+ expect(mockFn).toHaveBeenCalled();
+ });
+});
+// import test from 'ava';
+// import safeAwait from '../lib';
+
+// test('Valid promise has data. [err, data]', async t => {
+// const [err, data] = await safeAwait(promiseOne());
+// t.is(err, undefined);
+// t.is(data, 'data here');
+// });
+
+// test('Error promise has string error. [err, data]', async t => {
+// const [err, data] = await safeAwait(promiseOne(true));
+// t.is(err, 'error happened');
+// t.is(data, undefined);
+// });
+
+// test('Error promise has instance of error. [err, data]', async t => {
+// const [err, data] = await safeAwait(promiseThrows());
+// t.is(err instanceof Error, true);
+// t.is(data, undefined);
+// });
+
+// /**
+// * Verify promises still throw native errors when deeper issue exists
+// */
+
+// test('throws on code syntax error "ReferenceError"', async t => {
+// await t.throwsAsync(
+// async () => {
+// const [err, data] = await safeAwait(promiseWithSyntaxError());
+// },
+// {
+// instanceOf: ReferenceError,
+// message: 'madeUpThing is not defined',
+// }
+// );
+// });
+
+// test('throws on code syntax error "ReferenceError" in try/catch', async t => {
+// try {
+// const [err, data] = await safeAwait(promiseWithSyntaxError());
+// } catch (e) {
+// t.is(e instanceof ReferenceError, true);
+// }
+// });
+
+// test('throws on code "TypeError"', async t => {
+// await t.throwsAsync(
+// async () => {
+// const [err, data] = await safeAwait(promiseWithTypeError());
+// },
+// {
+// instanceOf: TypeError,
+// message: "Cannot read property 'lolCool' of null",
+// }
+// );
+// });
+
+// test('throws on code "TypeError" in try/catch', async t => {
+// try {
+// const [err, data] = await safeAwait(promiseWithTypeError());
+// } catch (e) {
+// t.is(e instanceof TypeError, true);
+// }
+// });
+
+// function promiseOne(doError) {
+// return new Promise((resolve, reject) => {
+// if (doError) return reject('error happened'); // eslint-disable-line
+// return resolve('data here');
+// });
+// }
+
+// function promiseThrows(doError) {
+// return new Promise((resolve, reject) => {
+// return reject(new Error('business logic error'));
+// });
+// }
+
+// function promiseWithSyntaxError() {
+// return new Promise((resolve, reject) => {
+// console.log(madeUpThing);
+// return resolve('should not reach this');
+// });
+// }
+
+// function promiseWithTypeError(doError) {
+// return new Promise((resolve, reject) => {
+// const fakeObject = null;
+// console.log(fakeObject.lolCool);
+// return resolve('should not reach this');
+// });
+// }
diff --git a/jest.config.js b/jest.config.js
index 03d0156..2db0e61 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -12,4 +12,12 @@ module.exports = {
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
moduleDirectories: ['node_modules', 'app/node_modules'],
setupFiles: ['./internals/scripts/CheckBuildsExist.js'],
+ globals: {
+ 'ts-jest': {
+ tsConfig: 'tsconfig.tests.json',
+ diagnostics: {
+ ignoreCodes: [6133],
+ },
+ },
+ },
};
diff --git a/package.json b/package.json
index 4a83620..8a3619c 100644
--- a/package.json
+++ b/package.json
@@ -110,10 +110,10 @@
"@babel/register": "7.10.1",
"@blockstack/eslint-config": "1.0.5",
"@blockstack/prettier-config": "0.0.6",
- "@blockstack/stacks-transactions": "0.4.6",
"@commitlint/config-conventional": "9.0.1",
"@types/bcryptjs": "2.4.2",
"@types/css-font-loading-module": "0.0.4",
+ "@types/electron-devtools-installer": "2.2.0",
"@types/enzyme": "3.10.5",
"@types/enzyme-adapter-react-16": "1.0.6",
"@types/history": "4.7.6",
@@ -187,6 +187,7 @@
"yarn": "1.22.4"
},
"dependencies": {
+ "@blockstack/stacks-transactions": "0.4.6",
"@blockstack/ui": "1.6.3",
"@hot-loader/react-dom": "16.13.0",
"@reduxjs/toolkit": "1.3.6",
diff --git a/tsconfig.json b/tsconfig.json
index 23f85e4..09b2cd7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -35,7 +35,8 @@
"app/main.js",
"app/main.js.map",
"node_modules/**",
- "app/node_modules/**"
+ "app/node_modules/**",
+ "**/*.spec.ts"
],
"typeRoots": ["./node_modules"]
}
diff --git a/tsconfig.tests.json b/tsconfig.tests.json
new file mode 100644
index 0000000..d86f259
--- /dev/null
+++ b/tsconfig.tests.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "noUnusedLocals": false,
+ "noUnusedParameters": false
+ },
+ "exclude": [],
+ "include": ["app/**/*.spec.ts"]
+}
diff --git a/yarn.lock b/yarn.lock
index 0241da2..32c8cf0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1821,6 +1821,11 @@
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd"
integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==
+"@types/electron-devtools-installer@2.2.0":
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/@types/electron-devtools-installer/-/electron-devtools-installer-2.2.0.tgz#32ee4ebbe99b3daf9847a6d2097dc00b5de94f10"
+ integrity sha512-HJNxpaOXuykCK4rQ6FOMxAA0NLFYsf7FiPFGmab0iQmtVBHSAfxzy3MRFpLTTDDWbV0yD2YsHOQvdu8yCqtCfw==
+
"@types/elliptic@^6.4.12":
version "6.4.12"
resolved "https://registry.yarnpkg.com/@types/elliptic/-/elliptic-6.4.12.tgz#e8add831f9cc9a88d9d84b3733ff669b68eaa124"
@@ -1927,9 +1932,9 @@
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/lodash@^4.14.149":
- version "4.14.155"
- resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a"
- integrity sha512-vEcX7S7aPhsBCivxMwAANQburHBtfN9RdyXFk84IJmu2Z4Hkg1tOFgaslRiEqqvoLtbCBi6ika1EMspE+NZ9Lg==
+ version "4.14.157"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.157.tgz#fdac1c52448861dfde1a2e1515dbc46e54926dc8"
+ integrity sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ==
"@types/minimatch@*":
version "3.0.3"
@@ -1976,13 +1981,6 @@
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
-"@types/ramda@0.27.6":
- version "0.27.6"
- resolved "https://registry.yarnpkg.com/@types/ramda/-/ramda-0.27.6.tgz#5b914266cb73ea61fe3778949ef9b0aa455804b4"
- integrity sha512-ephagb0ZIAJSoS5I/qMS4Mqo1b/Nd50pWM+o1QO/dz8NF//GsCGPTLDVRqgXlVncy74KShfHzE5rPZXTeek4PA==
- dependencies:
- ts-toolbelt "^6.3.3"
-
"@types/ramda@types/npm-ramda#dist":
version "0.25.0"
resolved "https://codeload.github.com/types/npm-ramda/tar.gz/9529aa3c8ff70ff84afcbc0be83443c00f30ea90"
@@ -4273,12 +4271,11 @@ cross-env@7.0.2:
cross-spawn "^7.0.1"
cross-fetch@^3.0.4:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.4.tgz#7bef7020207e684a7638ef5f2f698e24d9eb283c"
- integrity sha512-MSHgpjQqgbT/94D4CyADeNoYh52zMkCX4pcJvPP5WqPsLFMKjr2TCMg381ox5qI0ii2dPwaLx/00477knXqXVw==
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.5.tgz#2739d2981892e7ab488a7ad03b92df2816e03f4c"
+ integrity sha512-FFLcLtraisj5eteosnX1gf01qYDCOc4fDy0+euOt8Kn9YBY2NtXL/pCoYPavw24NIQkQqm5ZOLsGD5Zzj0gyew==
dependencies:
node-fetch "2.6.0"
- whatwg-fetch "3.0.0"
cross-spawn@6.0.5, cross-spawn@^6.0.0:
version "6.0.5"
@@ -12244,11 +12241,6 @@ ts-jest@26.1.1:
semver "7.x"
yargs-parser "18.x"
-ts-toolbelt@^6.3.3:
- version "6.9.9"
- resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-6.9.9.tgz#e6cfd8ec7d425d2a06bda3b4fe9577ceaf2abda8"
- integrity sha512-5a8k6qfbrL54N4Dw+i7M6kldrbjgDWb5Vit8DnT+gwThhvqMg8KtxLE5Vmnft+geIgaSOfNJyAcnmmlflS+Vdg==
-
tsconfig-paths@^3.9.0:
version "3.9.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"
@@ -12937,11 +12929,6 @@ whatwg-encoding@^1.0.5:
dependencies:
iconv-lite "0.4.24"
-whatwg-fetch@3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
- integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
-
whatwg-mimetype@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"