on master help sign in
feat: improve ethereum private key experience
ba40a8dMatt Condon (shrugs)
src/components/Inputs.js+33−14
@@ -1,4 +1,5 @@
1import React, { useState, useMemo } from 'react';
1import React, { useState, useMemo } from 'react';
2+import { AccessoryIcon, useForm } from 'indigo-react';
2import * as bip39 from 'bip39';
3import * as bip39 from 'bip39';
3
4
4import InputSigil from 'components/InputSigil';
5import InputSigil from 'components/InputSigil';
@@ -10,10 +11,13 @@ import {
10 validatePoint,
11 validatePoint,
11 validateTicket,
12 validateTicket,
12 validateEmail,
13 validateEmail,
14+ validateLength,
15+ validateHexString,
13} from 'lib/validators';
16} from 'lib/validators';
14import { prependSig } from 'lib/transformers';
17import { prependSig } from 'lib/transformers';
15−import useForm from 'indigo-react/lib/useForm';
18+
16−import { AccessoryIcon } from 'indigo-react';
19+const EXAMPLE_PRIVATE_KEY =
20+ '0xa44de2416ee6beb2f323fab48b432925c9785808d33a6ca6d7ba00b45e9370c3';
17
21
18// const RequiredInput = advancedInput({
22// const RequiredInput = advancedInput({
19// WrappedComponent: Input,
23// WrappedComponent: Input,
@@ -35,6 +39,12 @@ import { AccessoryIcon } from 'indigo-react';
35// validators: [validateEthereumAddress, validateNotEmpty],
39// validators: [validateEthereumAddress, validateNotEmpty],
36// });
40// });
37
41
42+// const GalaxyInput = advancedInput({
43+// WrappedComponent: Input,
44+// validators: [validateGalaxy, validateNotEmpty],
45+// transformers: [prependSig],
46+// });
47+
38const firstOf = state => state.inputs[0];
48const firstOf = state => state.inputs[0];
39
49
40export function usePassphraseInput(props) {
50export function usePassphraseInput(props) {
@@ -50,6 +60,27 @@ export function usePassphraseInput(props) {
50 );
60 );
51}
61}
52
62
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+
53const kExampleMnemonic = bip39.generateMnemonic();
84const kExampleMnemonic = bip39.generateMnemonic();
54const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
85const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
55export function useMnemonicInput(props) {
86export function useMnemonicInput(props) {
@@ -79,12 +110,6 @@ export function useHdPathInput(props) {
79 );
110 );
80}
111}
81
112
82−// const GalaxyInput = advancedInput({
83−// WrappedComponent: Input,
84−// validators: [validateGalaxy, validateNotEmpty],
85−// transformers: [prependSig],
86−// });
87−
88const kTicketValidators = [validateTicket, validateNotEmpty];
113const kTicketValidators = [validateTicket, validateNotEmpty];
89const kTicketTransformers = [prependSig];
114const kTicketTransformers = [prependSig];
90//TODO needs to be fancier, displaying sig and dashes instead of •ing all
115//TODO needs to be fancier, displaying sig and dashes instead of •ing all
@@ -174,9 +199,3 @@ export const buildEmailInputConfig = extra => ({
174 initialValue: '',
199 initialValue: '',
175 ...extra,
200 ...extra,
176});
201});
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';
5import Footer from './Footer';
5import Footer from './Footer';
6
6
7// View is a top-level component that all Views must render to inherit styling
7// 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 }) {
9 return (
9 return (
10 <Flex
10 <Flex
11 col
11 col
@@ -19,7 +19,13 @@ function View({ className, children, full = false, ...rest }) {
19 className
19 className
20 )}
20 )}
21 {...rest}>
21 {...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>
23 <Footer.Target />
29 <Footer.Target />
24 </Flex>
30 </Flex>
25 );
31 );
src/indigo-react/index.js+2
@@ -8,3 +8,5 @@ export { default as Button } from './components/Button';
8export { default as IconButton } from './components/IconButton';
8export { default as IconButton } from './components/IconButton';
9
9
10export * from './components/Typography';
10export * 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';
5import { isValidAddress } from './wallet';
5import { isValidAddress } from './wallet';
6import patp2dec from './patp2dec';
6import patp2dec from './patp2dec';
7
7
8+// NOTE: do not use the /g modifier for these regexes
9+// https://stackoverflow.com/a/21373261
10+// https://stackoverflow.com/a/1520853
11+
8// via: https://emailregex.com/
12// via: https://emailregex.com/
9const 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,}))$/;
13const 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,}))$/;
10
14
11// is this 64 hex chars?
15// 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]/;
13
20
14// Wraps single validation functions in a controlled and predictable way.
21// Wraps single validation functions in a controlled and predictable way.
15export const simpleValidatorWrapper = ({
22export const simpleValidatorWrapper = ({
@@ -129,7 +136,7 @@ export const validateShard = m =>
129 error: 'This is not a valid shard',
136 error: 'This is not a valid shard',
130 });
137 });
131
138
132−export const validateLength = (m, l) =>
139+export const validateLength = l => m =>
133 simpleValidatorWrapper({
140 simpleValidatorWrapper({
134 prevMessage: m,
141 prevMessage: m,
135 validator: d => {
142 validator: d => {
@@ -139,20 +146,29 @@ export const validateLength = (m, l) =>
139 return false;
146 return false;
140 }
147 }
141 },
148 },
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',
143 });
157 });
144
158
159+// @deprecate
145export const validateNetworkKey = m =>
160export const validateNetworkKey = m =>
146 simpleValidatorWrapper({
161 simpleValidatorWrapper({
147 prevMessage: m,
162 prevMessage: m,
148− validator: d => hexRegExp.test(d),
163+ validator: d => plain64CharHexValue.test(d),
149 error: 'This is not a valid network key',
164 error: 'This is not a valid network key',
150 });
165 });
151
166
167+// @deprecate
152export const validateNetworkSeed = m =>
168export const validateNetworkSeed = m =>
153 simpleValidatorWrapper({
169 simpleValidatorWrapper({
154 prevMessage: m,
170 prevMessage: m,
155− validator: d => hexRegExp.test(d),
171+ validator: d => plain64CharHexValue.test(d),
156 error: 'This is not a valid network seed',
172 error: 'This is not a valid network seed',
157 });
173 });
158
174
src/lib/wallet.js+20−52
@@ -8,13 +8,12 @@ import * as ob from 'urbit-ob';
8import * as kg from 'urbit-key-generation/dist';
8import * as kg from 'urbit-key-generation/dist';
9import { isAddress } from 'web3-utils';
9import { isAddress } from 'web3-utils';
10
10
11−const DEFAULT_HD_PATH = "m/44'/60'/0'/0/0";
11+export const DEFAULT_HD_PATH = "m/44'/60'/0'/0/0";
12−
13−const ETH_ZERO_ADDR = '0x0000000000000000000000000000000000000000';
12+export const ETH_ZERO_ADDR = '0x0000000000000000000000000000000000000000';
14−const CURVE_ZERO_ADDR =
13+export const CURVE_ZERO_ADDR =
15 '0x0000000000000000000000000000000000000000000000000000000000000000';
14 '0x0000000000000000000000000000000000000000000000000000000000000000';
16
15
17−const WALLET_TYPES = {
16+export const WALLET_TYPES = {
18 MNEMONIC: Symbol('MNEMONIC'),
17 MNEMONIC: Symbol('MNEMONIC'),
19 TICKET: Symbol('TICKET'),
18 TICKET: Symbol('TICKET'),
20 SHARDS: Symbol('SHARDS'),
19 SHARDS: Symbol('SHARDS'),
@@ -24,31 +23,14 @@ const WALLET_TYPES = {
24 KEYSTORE: Symbol('KEYSTORE'),
23 KEYSTORE: Symbol('KEYSTORE'),
25};
24};
26
25
27−function EthereumWallet(privateKey) {
26+export function EthereumWallet(privateKey) {
28 this.privateKey = privateKey;
27 this.privateKey = privateKey;
29 this.publicKey = secp256k1.publicKeyCreate(this.privateKey);
28 this.publicKey = secp256k1.publicKeyCreate(this.privateKey);
30 const pub = this.publicKey.toString('hex');
29 const pub = this.publicKey.toString('hex');
31 this.address = addressFromSecp256k1Public(pub);
30 this.address = addressFromSecp256k1Public(pub);
32}
31}
33
32
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 => {
52 const compressed = false;
34 const compressed = false;
53 const uncompressed = secp256k1.publicKeyConvert(
35 const uncompressed = secp256k1.publicKeyConvert(
54 Buffer.from(pub, 'hex'),
36 Buffer.from(pub, 'hex'),
@@ -60,18 +42,20 @@ const addressFromSecp256k1Public = pub => {
60 return toChecksumAddress(addr);
42 return toChecksumAddress(addr);
61};
43};
62
44
45+export const addHexPrefix = hex =>
63−const addHexPrefix = hex => (hex.slice(0, 2) === '0x' ? hex : '0x' + hex);
46+ hex.slice(0, 2) === '0x' ? hex : '0x' + hex;
64
47
48+export const stripHexPrefix = hex =>
65−const stripHexPrefix = hex => (hex.slice(0, 2) === '0x' ? hex.slice(2) : hex);
49+ hex.slice(0, 2) === '0x' ? hex.slice(2) : hex;
66
50
67−const keccak256 = str =>
51+export const keccak256 = str =>
68 keccak('keccak256')
52 keccak('keccak256')
69 .update(str)
53 .update(str)
70 .digest();
54 .digest();
71
55
72−const isValidAddress = a => '0x0' === a || isAddress(a);
56+export const isValidAddress = a => '0x0' === a || isAddress(a);
73
57
74−const toChecksumAddress = address => {
58+export const toChecksumAddress = address => {
75 const addr = stripHexPrefix(address).toLowerCase();
59 const addr = stripHexPrefix(address).toLowerCase();
76 const hash = keccak256(addr).toString('hex');
60 const hash = keccak256(addr).toString('hex');
77
61
@@ -83,27 +67,28 @@ const toChecksumAddress = address => {
83 );
67 );
84};
68};
85
69
86−const eqAddr = (addr0, addr1) =>
70+export const eqAddr = (addr0, addr1) =>
87 toChecksumAddress(addr0) === toChecksumAddress(addr1);
71 toChecksumAddress(addr0) === toChecksumAddress(addr1);
88
72
89−const urbitWalletFromTicket = async (ticket, point, passphrase) => {
73+export const urbitWalletFromTicket = async (ticket, point, passphrase) => {
90 if (typeof point === 'string') {
74 if (typeof point === 'string') {
91 //TODO why not in kg?
75 //TODO why not in kg?
92 point = ob.patp2dec(point);
76 point = ob.patp2dec(point);
93 }
77 }
78+
94− let uhdw = await kg.generateWallet({
79+ return await kg.generateWallet({
95 ticket: ticket,
80 ticket: ticket,
96 ship: point,
81 ship: point,
97 passphrase: passphrase,
82 passphrase: passphrase,
98 });
83 });
99− return uhdw;
100};
84};
101
85
102−const ownershipWalletFromTicket = async (ticket, point, passphrase) => {
86+export const ownershipWalletFromTicket = async (ticket, point, passphrase) => {
103 if (typeof point === 'string') {
87 if (typeof point === 'string') {
104 //TODO why not in kg?
88 //TODO why not in kg?
105 point = ob.patp2dec(point);
89 point = ob.patp2dec(point);
106 }
90 }
91+
107 return await kg.generateOwnershipWallet({
92 return await kg.generateOwnershipWallet({
108 ticket,
93 ticket,
109 ship: point,
94 ship: point,
@@ -111,7 +96,7 @@ const ownershipWalletFromTicket = async (ticket, point, passphrase) => {
111 });
96 });
112};
97};
113
98
114−const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
99+export const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
115 const seed = bip39.validateMnemonic(mnemonic)
100 const seed = bip39.validateMnemonic(mnemonic)
116 ? Maybe.Just(bip39.mnemonicToSeed(mnemonic, passphrase))
101 ? Maybe.Just(bip39.mnemonicToSeed(mnemonic, passphrase))
117 : Maybe.Nothing();
102 : Maybe.Nothing();
@@ -132,20 +117,3 @@ const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
132 const wallet = seed.chain(sd => toWallet(sd, hdpath));
117 const wallet = seed.chain(sd => toWallet(sd, hdpath));
133 return wallet;
118 return wallet;
134};
119};
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() {
102 }, [contracts, pointCursor, setPointCursor, wallet, goToPoint, goToPoints]);
102 }, [contracts, pointCursor, setPointCursor, wallet, goToPoint, goToPoints]);
103
103
104 return (
104 return (
105− <View>
105+ <View inset>
106 <Grid>
106 <Grid>
107 <Grid.Item full as={Crumbs} routes={[{ text: 'Multipass' }]} />
107 <Grid.Item full as={Crumbs} routes={[{ text: 'Multipass' }]} />
108 <Grid.Item full as={H4} className="mt4">
108 <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';
2import React, { useEffect } from 'react';
1import React, { useEffect } from 'react';
2+import Maybe from 'folktale/maybe';
3import { Grid, Input } from 'indigo-react';
3import { Grid, Input } from 'indigo-react';
4
4
5−import { usePassphraseInput } from 'components/Inputs';
5+import { useHexInput } from 'components/Inputs';
6
6
7import { useWallet } from 'store/wallet';
7import { useWallet } from 'store/wallet';
8
8
9−import { EthereumWallet, WALLET_TYPES } from 'lib/wallet';
9+import { EthereumWallet, WALLET_TYPES, stripHexPrefix } from 'lib/wallet';
10import useLoginView from 'lib/useLoginView';
10import useLoginView from 'lib/useLoginView';
11
11
12export default function PrivateKey({ className }) {
12export default function PrivateKey({ className }) {
@@ -14,22 +14,23 @@ export default function PrivateKey({ className }) {
14
14
15 const { setWallet } = useWallet();
15 const { setWallet } = useWallet();
16
16
17− const privateKeyInput = usePassphraseInput({
17+ const privateKeyInput = useHexInput({
18+ length: 64,
18 name: 'privateKey',
19 name: 'privateKey',
19 label: 'Private key',
20 label: 'Private key',
20 autoFocus: true,
21 autoFocus: true,
21 });
22 });
22− const privateKey = privateKeyInput.data;
23+ const { pass, data: privateKey } = privateKeyInput;
23
24
24 useEffect(() => {
25 useEffect(() => {
25− if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
26+ if (pass) {
26− const sec = Buffer.from(privateKey, 'hex');
27+ const sec = Buffer.from(stripHexPrefix(privateKey), 'hex');
27 const newWallet = new EthereumWallet(sec);
28 const newWallet = new EthereumWallet(sec);
28− setWallet(Just(newWallet));
29+ setWallet(Maybe.Just(newWallet));
29 } else {
30 } else {
30− setWallet(Nothing());
31+ setWallet(Maybe.Nothing());
31 }
32 }
32− }, [privateKey, setWallet]);
33+ }, [pass, privateKey, setWallet]);
33
34
34 return (
35 return (
35 <Grid className={className}>
36 <Grid className={className}>