on master help sign in
feat: improve ethereum private key experience
commitba40a8d09c6e743e975598c7c01f8ad995d59fb7 authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateTue 2 Jul 2019, 13:53 (7 years ago) parent9781501
8 files changed+97−84
src/components/Inputs.js+33−14 src/components/View.js+8−2 src/indigo-react/index.js+2 src/indigo-react/lib/index.js+1 src/lib/validators.js+21−5 src/lib/wallet.js+20−52 src/views/Login.js+1−1 src/views/Login/PrivateKey.js+11−10
src/components/Inputs.js+33−14
@@ -1,4 +1,5 @@
11import React, { useState, useMemo } from 'react';
2+import { AccessoryIcon, useForm } from 'indigo-react';
23import * as bip39 from 'bip39';
34
45import InputSigil from 'components/InputSigil';
@@ -10,10 +11,13 @@ import {
1011 validatePoint,
1112 validateTicket,
1213 validateEmail,
14+ validateLength,
15+ validateHexString,
1316} from 'lib/validators';
1417import { prependSig } from 'lib/transformers';
15−import useForm from 'indigo-react/lib/useForm';
16−import { AccessoryIcon } from 'indigo-react';
18+
19+const EXAMPLE_PRIVATE_KEY =
20+ '0xa44de2416ee6beb2f323fab48b432925c9785808d33a6ca6d7ba00b45e9370c3';
1721
1822// const RequiredInput = advancedInput({
1923// WrappedComponent: Input,
@@ -35,6 +39,12 @@ import { AccessoryIcon } from 'indigo-react';
3539// validators: [validateEthereumAddress, validateNotEmpty],
3640// });
3741
42+// const GalaxyInput = advancedInput({
43+// WrappedComponent: Input,
44+// validators: [validateGalaxy, validateNotEmpty],
45+// transformers: [prependSig],
46+// });
47+
3848const firstOf = state => state.inputs[0];
3949
4050export function usePassphraseInput(props) {
@@ -50,6 +60,27 @@ export function usePassphraseInput(props) {
5060 );
5161}
5262
63+export function useHexInput({ length, ...rest }) {
64+ return firstOf(
65+ useForm([
66+ {
67+ type: 'text', // or password
68+ autoComplete: 'off',
69+ placeholder: EXAMPLE_PRIVATE_KEY,
70+ validators: useMemo(
71+ () => [
72+ validateHexString,
73+ validateLength(length + 2),
74+ validateNotEmpty,
75+ ],
76+ [length]
77+ ),
78+ ...rest,
79+ },
80+ ])
81+ );
82+}
83+
5384const kExampleMnemonic = bip39.generateMnemonic();
5485const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
5586export function useMnemonicInput(props) {
@@ -79,12 +110,6 @@ export function useHdPathInput(props) {
79110 );
80111}
81112
82−// const GalaxyInput = advancedInput({
83−// WrappedComponent: Input,
84−// validators: [validateGalaxy, validateNotEmpty],
85−// transformers: [prependSig],
86−// });
87−
88113const kTicketValidators = [validateTicket, validateNotEmpty];
89114const kTicketTransformers = [prependSig];
90115//TODO needs to be fancier, displaying sig and dashes instead of •ing all
@@ -174,9 +199,3 @@ export const buildEmailInputConfig = extra => ({
174199 initialValue: '',
175200 ...extra,
176201});
177−
178−// const TicketInput = advancedInput({
179−// WrappedComponent: Input,
180−// validators: [validateTicket, validateNotEmpty],
181−// transformers: [prependSig],
182−// });
src/components/View.js+8−2
@@ -5,7 +5,7 @@ import { Flex } from 'indigo-react';
55import Footer from './Footer';
66
77// View is a top-level component that all Views must render to inherit styling
8−function View({ className, children, full = false, ...rest }) {
8+function View({ className, children, inset = false, full = false, ...rest }) {
99 return (
1010 <Flex
1111 col
@@ -19,7 +19,13 @@ function View({ className, children, full = false, ...rest }) {
1919 className
2020 )}
2121 {...rest}>
22− <Flex.Item className="pv5">{children}</Flex.Item>
22+ <Flex.Item
23+ className={cn('pb5', {
24+ pt10: inset,
25+ pt5: !inset,
26+ })}>
27+ {children}
28+ </Flex.Item>
2329 <Footer.Target />
2430 </Flex>
2531 );
src/indigo-react/index.js+2
@@ -8,3 +8,5 @@ export { default as Button } from './components/Button';
88export { default as IconButton } from './components/IconButton';
99
1010export * from './components/Typography';
11+
12+export * from './lib';
src/indigo-react/lib/index.jsnew file+1
@@ -0,0 +1 @@
1+export { default as useForm } from './useForm';
src/lib/validators.js+21−5
@@ -5,11 +5,18 @@ import { identity } from 'lodash';
55import { isValidAddress } from './wallet';
66import patp2dec from './patp2dec';
77
8+// NOTE: do not use the /g modifier for these regexes
9+// https://stackoverflow.com/a/21373261
10+// https://stackoverflow.com/a/1520853
11+
812// via: https://emailregex.com/
913const emailRegExp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
1014
1115// is this 64 hex chars?
12−const hexRegExp = /[0-9A-Fa-f]{64}/g;
16+const plain64CharHexValue = /[0-9A-Fa-f]{64}/;
17+
18+// is this only hex values with a 0x prefix?
19+const isHexString = /0x[0-9A-Fa-f]/;
1320
1421// Wraps single validation functions in a controlled and predictable way.
1522export const simpleValidatorWrapper = ({
@@ -129,7 +136,7 @@ export const validateShard = m =>
129136 error: 'This is not a valid shard',
130137 });
131138
132−export const validateLength = (m, l) =>
139+export const validateLength = l => m =>
133140 simpleValidatorWrapper({
134141 prevMessage: m,
135142 validator: d => {
@@ -139,20 +146,29 @@ export const validateLength = (m, l) =>
139146 return false;
140147 }
141148 },
142− error: 'This is of an invalid length',
149+ error: `Must be exactly ${l} characters`,
150+ });
151+
152+export const validateHexString = m =>
153+ simpleValidatorWrapper({
154+ prevMessage: m,
155+ validator: d => isHexString.test(d),
156+ error: 'This is not a valid hex string',
143157 });
144158
159+// @deprecate
145160export const validateNetworkKey = m =>
146161 simpleValidatorWrapper({
147162 prevMessage: m,
148− validator: d => hexRegExp.test(d),
163+ validator: d => plain64CharHexValue.test(d),
149164 error: 'This is not a valid network key',
150165 });
151166
167+// @deprecate
152168export const validateNetworkSeed = m =>
153169 simpleValidatorWrapper({
154170 prevMessage: m,
155− validator: d => hexRegExp.test(d),
171+ validator: d => plain64CharHexValue.test(d),
156172 error: 'This is not a valid network seed',
157173 });
158174
src/lib/wallet.js+20−52
@@ -8,13 +8,12 @@ import * as ob from 'urbit-ob';
88import * as kg from 'urbit-key-generation/dist';
99import { isAddress } from 'web3-utils';
1010
11−const DEFAULT_HD_PATH = "m/44'/60'/0'/0/0";
12−
13−const ETH_ZERO_ADDR = '0x0000000000000000000000000000000000000000';
14−const CURVE_ZERO_ADDR =
11+export const DEFAULT_HD_PATH = "m/44'/60'/0'/0/0";
12+export const ETH_ZERO_ADDR = '0x0000000000000000000000000000000000000000';
13+export const CURVE_ZERO_ADDR =
1514 '0x0000000000000000000000000000000000000000000000000000000000000000';
1615
17−const WALLET_TYPES = {
16+export const WALLET_TYPES = {
1817 MNEMONIC: Symbol('MNEMONIC'),
1918 TICKET: Symbol('TICKET'),
2019 SHARDS: Symbol('SHARDS'),
@@ -24,31 +23,14 @@ const WALLET_TYPES = {
2423 KEYSTORE: Symbol('KEYSTORE'),
2524};
2625
27−function EthereumWallet(privateKey) {
26+export function EthereumWallet(privateKey) {
2827 this.privateKey = privateKey;
2928 this.publicKey = secp256k1.publicKeyCreate(this.privateKey);
3029 const pub = this.publicKey.toString('hex');
3130 this.address = addressFromSecp256k1Public(pub);
3231}
3332
34−const renderWalletType = wallet =>
35− wallet === WALLET_TYPES.MNEMONIC
36− ? 'Mnemonic'
37− : wallet === WALLET_TYPES.TICKET
38− ? 'Ticket'
39− : wallet === WALLET_TYPES.SHARDS
40− ? 'Ticket Shards'
41− : wallet === WALLET_TYPES.LEDGER
42− ? 'Ledger'
43− : wallet === WALLET_TYPES.TREZOR
44− ? 'Trezor'
45− : wallet === WALLET_TYPES.PRIVATE_KEY
46− ? 'Private Key'
47− : wallet === WALLET_TYPES.KEYSTORE
48− ? 'Keystore File'
49− : 'Wallet';
50−
51−const addressFromSecp256k1Public = pub => {
33+export const addressFromSecp256k1Public = pub => {
5234 const compressed = false;
5335 const uncompressed = secp256k1.publicKeyConvert(
5436 Buffer.from(pub, 'hex'),
@@ -60,18 +42,20 @@ const addressFromSecp256k1Public = pub => {
6042 return toChecksumAddress(addr);
6143};
6244
63−const addHexPrefix = hex => (hex.slice(0, 2) === '0x' ? hex : '0x' + hex);
45+export const addHexPrefix = hex =>
46+ hex.slice(0, 2) === '0x' ? hex : '0x' + hex;
6447
65−const stripHexPrefix = hex => (hex.slice(0, 2) === '0x' ? hex.slice(2) : hex);
48+export const stripHexPrefix = hex =>
49+ hex.slice(0, 2) === '0x' ? hex.slice(2) : hex;
6650
67−const keccak256 = str =>
51+export const keccak256 = str =>
6852 keccak('keccak256')
6953 .update(str)
7054 .digest();
7155
72−const isValidAddress = a => '0x0' === a || isAddress(a);
56+export const isValidAddress = a => '0x0' === a || isAddress(a);
7357
74−const toChecksumAddress = address => {
58+export const toChecksumAddress = address => {
7559 const addr = stripHexPrefix(address).toLowerCase();
7660 const hash = keccak256(addr).toString('hex');
7761
@@ -83,27 +67,28 @@ const toChecksumAddress = address => {
8367 );
8468};
8569
86−const eqAddr = (addr0, addr1) =>
70+export const eqAddr = (addr0, addr1) =>
8771 toChecksumAddress(addr0) === toChecksumAddress(addr1);
8872
89−const urbitWalletFromTicket = async (ticket, point, passphrase) => {
73+export const urbitWalletFromTicket = async (ticket, point, passphrase) => {
9074 if (typeof point === 'string') {
9175 //TODO why not in kg?
9276 point = ob.patp2dec(point);
9377 }
94− let uhdw = await kg.generateWallet({
78+
79+ return await kg.generateWallet({
9580 ticket: ticket,
9681 ship: point,
9782 passphrase: passphrase,
9883 });
99− return uhdw;
10084};
10185
102−const ownershipWalletFromTicket = async (ticket, point, passphrase) => {
86+export const ownershipWalletFromTicket = async (ticket, point, passphrase) => {
10387 if (typeof point === 'string') {
10488 //TODO why not in kg?
10589 point = ob.patp2dec(point);
10690 }
91+
10792 return await kg.generateOwnershipWallet({
10893 ticket,
10994 ship: point,
@@ -111,7 +96,7 @@ const ownershipWalletFromTicket = async (ticket, point, passphrase) => {
11196 });
11297};
11398
114−const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
99+export const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
115100 const seed = bip39.validateMnemonic(mnemonic)
116101 ? Maybe.Just(bip39.mnemonicToSeed(mnemonic, passphrase))
117102 : Maybe.Nothing();
@@ -132,20 +117,3 @@ const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
132117 const wallet = seed.chain(sd => toWallet(sd, hdpath));
133118 return wallet;
134119};
135−
136−export {
137− DEFAULT_HD_PATH,
138− WALLET_TYPES,
139− ETH_ZERO_ADDR,
140− CURVE_ZERO_ADDR,
141− renderWalletType,
142− isValidAddress,
143− toChecksumAddress,
144− addressFromSecp256k1Public,
145− eqAddr,
146− urbitWalletFromTicket,
147− ownershipWalletFromTicket,
148− walletFromMnemonic,
149− addHexPrefix,
150− EthereumWallet,
151−};
src/views/Login.js+1−1
@@ -102,7 +102,7 @@ export default function Login() {
102102 }, [contracts, pointCursor, setPointCursor, wallet, goToPoint, goToPoints]);
103103
104104 return (
105− <View>
105+ <View inset>
106106 <Grid>
107107 <Grid.Item full as={Crumbs} routes={[{ text: 'Multipass' }]} />
108108 <Grid.Item full as={H4} className="mt4">
src/views/Login/PrivateKey.js+11−10
@@ -1,12 +1,12 @@
1−import { Just, Nothing } from 'folktale/maybe';
21import React, { useEffect } from 'react';
2+import Maybe from 'folktale/maybe';
33import { Grid, Input } from 'indigo-react';
44
5−import { usePassphraseInput } from 'components/Inputs';
5+import { useHexInput } from 'components/Inputs';
66
77import { useWallet } from 'store/wallet';
88
9−import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
9+import { EthereumWallet, WALLET_TYPES, stripHexPrefix } from 'lib/wallet';
1010import useLoginView from 'lib/useLoginView';
1111
1212export default function PrivateKey({ className }) {
@@ -14,22 +14,23 @@ export default function PrivateKey({ className }) {
1414
1515 const { setWallet } = useWallet();
1616
17− const privateKeyInput = usePassphraseInput({
17+ const privateKeyInput = useHexInput({
18+ length: 64,
1819 name: 'privateKey',
1920 label: 'Private key',
2021 autoFocus: true,
2122 });
22− const privateKey = privateKeyInput.data;
23+ const { pass, data: privateKey } = privateKeyInput;
2324
2425 useEffect(() => {
25− if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
26− const sec = Buffer.from(privateKey, 'hex');
26+ if (pass) {
27+ const sec = Buffer.from(stripHexPrefix(privateKey), 'hex');
2728 const newWallet = new EthereumWallet(sec);
28− setWallet(Just(newWallet));
29+ setWallet(Maybe.Just(newWallet));
2930 } else {
30− setWallet(Nothing());
31+ setWallet(Maybe.Nothing());
3132 }
32− }, [privateKey, setWallet]);
33+ }, [pass, privateKey, setWallet]);
3334
3435 return (
3536 <Grid className={className}>