on master help sign in
feat: improve useForm memoization, update useFirstOf api, clean up Ledger/Trezor views
commitcd9162814ad91f911da777bdeb993eecced22bd6 authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateTue 2 Jul 2019, 15:33 (7 years ago) parentba40a8d
17 files changed+312−368
src/components/Footer.js+2−1 src/components/Inputs.js+58−40 src/indigo-react/components/CheckboxInput.js+1 src/indigo-react/components/Input.js+1 src/indigo-react/lib/useForm.js+21−9 src/lib/ledger.js+6−6 src/views/Activate/ActivateCode.js+1−2 src/views/Activate/ActivateDisclaimer.js+3−3 src/views/Activate/PassportVerify.js+1−2 src/views/Login/Keystore.js+1−2 src/views/Login/Ledger.js+102−87 src/views/Login/Mnemonic.js+25−27 src/views/Login/PrivateKey.js+1−2 src/views/Login/Shards.js−96 src/views/Login/Ticket.js+23−42 src/views/Login/Trezor.js+65−47 src/views/ViewPoint.js+1−2
src/components/Footer.js+2−1
@@ -1,8 +1,9 @@
1+import React from 'react';
12import { createTeleporter } from 'react-teleporter';
23
34const FooterTeleporter = createTeleporter();
45
56const Footer = FooterTeleporter.Source;
6−Footer.Target = FooterTeleporter.Target;
7+Footer.Target = props => <FooterTeleporter.Target as="footer" {...props} />;
78
89export default Footer;
src/components/Inputs.js+58−40
@@ -1,5 +1,6 @@
1−import React, { useState, useMemo } from 'react';
1+import React, { useCallback, useState, useMemo } from 'react';
22import { AccessoryIcon, useForm } from 'indigo-react';
3+import { identity } from 'lodash';
34import * as bip39 from 'bip39';
45
56import InputSigil from 'components/InputSigil';
@@ -45,10 +46,35 @@ const EXAMPLE_PRIVATE_KEY =
4546// transformers: [prependSig],
4647// });
4748
48−const firstOf = state => state.inputs[0];
49+// pulls out the first input from a useForm() call
50+function useFirstOf({ inputs, setValue, ...rest }, mapper = identity) {
51+ // ask the mapper function for any values to overwrite
52+ const input = {
53+ ...inputs[0],
54+ ...mapper(inputs[0]),
55+ };
56+ // memoize the setValue callback for manually setting the value
57+ // in response to some imperative event
58+ const _setValue = useCallback(value => setValue(input.name, value), [
59+ setValue,
60+ input.name,
61+ ]);
62+
63+ // provide the first input as the first element
64+ // _and_ as the second element to facilitate destructuring
65+ // and then provide the pass/error/setValue/etc properties at the end
66+ return [
67+ input,
68+ input,
69+ {
70+ ...rest,
71+ setValue: _setValue,
72+ },
73+ ];
74+}
4975
5076export function usePassphraseInput(props) {
51− return firstOf(
77+ return useFirstOf(
5278 useForm([
5379 {
5480 type: 'password',
@@ -61,7 +87,7 @@ export function usePassphraseInput(props) {
6187}
6288
6389export function useHexInput({ length, ...rest }) {
64− return firstOf(
90+ return useFirstOf(
6591 useForm([
6692 {
6793 type: 'text', // or password
@@ -84,7 +110,7 @@ export function useHexInput({ length, ...rest }) {
84110const kExampleMnemonic = bip39.generateMnemonic();
85111const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
86112export function useMnemonicInput(props) {
87− return firstOf(
113+ return useFirstOf(
88114 useForm([
89115 {
90116 type: 'text',
@@ -98,7 +124,7 @@ export function useMnemonicInput(props) {
98124}
99125
100126export function useHdPathInput(props) {
101− return firstOf(
127+ return useFirstOf(
102128 useForm([
103129 {
104130 type: 'text',
@@ -114,7 +140,7 @@ const kTicketValidators = [validateTicket, validateNotEmpty];
114140const kTicketTransformers = [prependSig];
115141//TODO needs to be fancier, displaying sig and dashes instead of •ing all
116142export function useTicketInput({ validators = [], deriving = false, ...rest }) {
117− const input = firstOf(
143+ return useFirstOf(
118144 useForm([
119145 {
120146 type: 'password',
@@ -127,28 +153,24 @@ export function useTicketInput({ validators = [], deriving = false, ...rest }) {
127153 mono: true,
128154 ...rest,
129155 },
130− ])
156+ ]),
157+ ({ error, pass }) => ({
158+ accessory: error ? (
159+ <AccessoryIcon.Failure />
160+ ) : deriving ? (
161+ <AccessoryIcon.Pending />
162+ ) : pass ? (
163+ <AccessoryIcon.Success />
164+ ) : null,
165+ })
131166 );
132−
133− const accessory = input.error ? (
134− <AccessoryIcon.Failure />
135− ) : deriving ? (
136− <AccessoryIcon.Pending />
137− ) : input.pass ? (
138− <AccessoryIcon.Success />
139− ) : null;
140−
141− return {
142− ...input,
143− accessory,
144− };
145167}
146168
147169const kPointValidators = [validatePoint, validateNotEmpty];
148170const kPointTransformers = [prependSig];
149171export function usePointInput(rest) {
150172 const [lastValidPoint, setLastValidPoint] = useState('');
151− const input = firstOf(
173+ return useFirstOf(
152174 useForm([
153175 {
154176 type: 'text',
@@ -160,28 +182,24 @@ export function usePointInput(rest) {
160182 onValue: setLastValidPoint,
161183 ...rest,
162184 },
163− ])
185+ ]),
186+ ({ error, pass, focused }) => ({
187+ accessory: lastValidPoint ? (
188+ <InputSigil
189+ patp={lastValidPoint}
190+ size={68}
191+ margin={8}
192+ pass={pass}
193+ focused={focused}
194+ error={error}
195+ />
196+ ) : null,
197+ })
164198 );
165−
166− const { pass, focused, error } = input;
167−
168− return {
169− ...input,
170− accessory: lastValidPoint && (
171− <InputSigil
172− patp={lastValidPoint}
173− size={68}
174− margin={8}
175− pass={pass}
176− focused={focused}
177− error={error}
178− />
179− ),
180− };
181199}
182200
183201export function useCheckboxInput({ initialValue, ...rest }) {
184− return firstOf(
202+ return useFirstOf(
185203 useForm([
186204 {
187205 type: 'checkbox',
src/indigo-react/components/CheckboxInput.js+1
@@ -31,6 +31,7 @@ export default function CheckboxInput({
3131 initialValue,
3232 validators,
3333 transformers,
34+ setValue,
3435
3536 // extra
3637 ...rest
src/indigo-react/components/Input.js+1
@@ -33,6 +33,7 @@ export default function Input({
3333 initialValue,
3434 validators,
3535 transformers,
36+ setValue,
3637
3738 // extra
3839 ...rest
src/indigo-react/lib/useForm.js+21−9
@@ -1,9 +1,10 @@
11import { useCallback, useMemo, useState } from 'react';
2−import { keyBy, get, every, some } from 'lodash';
2+import { isEqual, keyBy, get, every, some } from 'lodash';
33
44import { compose } from 'lib/lib';
55import { kDefaultValidator } from 'lib/validators';
66import useSetState from 'lib/useSetState';
7+import usePreviousValue from 'lib/usePreviousValue';
78
89// interface InputConfig {
910// name: string;
@@ -24,7 +25,15 @@ const defaultsFor = (configs, mapper) =>
2425/**
2526 * useForm manages a set of inputs for rendering them in a loop
2627 */
27−export default function useForm(configs = []) {
28+export default function useForm(inputConfigs = []) {
29+ // track the old value of the config set
30+ const previousConfigs = usePreviousValue(inputConfigs);
31+ // then compare equality
32+ const configsAreEqual = isEqual(inputConfigs, previousConfigs);
33+ // if equality changes, give `configs` a new identity
34+ // eslint-disable-next-line react-hooks/exhaustive-deps
35+ const configs = useMemo(() => inputConfigs, [configsAreEqual]);
36+
2837 const byName = useMemo(() => keyBy(configs, 'name'), [configs]);
2938
3039 // track values
@@ -38,11 +47,6 @@ export default function useForm(configs = []) {
3847 // but for dynamically added input configs, we won't get validation, etc
3948 // until there's a traditional state update.
4049
41− const setValue = useCallback(
42− (name, value) => _setValues(values => ({ ...values, [name]: value })),
43− [_setValues]
44− );
45−
4650 // track focused states
4751 const [focused, setFocused] = useSetState(() =>
4852 defaultsFor(configs, config => config.autoFocus && !config.disabled)
@@ -61,6 +65,13 @@ export default function useForm(configs = []) {
6165 [byName]
6266 );
6367
68+ // set a value (and transform it)
69+ const setValue = useCallback(
70+ (name, value) =>
71+ _setValues(values => ({ ...values, [name]: transform(name, value) })),
72+ [_setValues, transform]
73+ );
74+
6475 // build fn that validates a value by input name
6576 const validate = useCallback(
6677 (name, value) =>
@@ -84,8 +95,8 @@ export default function useForm(configs = []) {
8495
8596 // on change, transform and set value
8697 const onChange = useCallback(
87− name => e => setValue(name, transform(name, getValue(name, e))), //
88− [setValue, transform, getValue]
98+ name => e => setValue(name, getValue(name, e)), //
99+ [setValue, getValue]
89100 );
90101
91102 // on focus, update focus
@@ -198,5 +209,6 @@ export default function useForm(configs = []) {
198209 inputs,
199210 pass,
200211 error,
212+ setValue,
201213 };
202214}
src/lib/ledger.js+6−6
@@ -1,12 +1,14 @@
11import Transport from '@ledgerhq/hw-transport-u2f';
22import Eth from '@ledgerhq/hw-app-eth';
33
4−const LEDGER_LEGACY_PATH = "m/44'/60'/0'/x";
5−const LEDGER_LIVE_PATH = "m/44'/60'/x'/0/0";
4+export const LEDGER_LEGACY_PATH = "m/44'/60'/0'/x";
5+export const LEDGER_LIVE_PATH = "m/44'/60'/x'/0/0";
66
7−const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
7+export const chopHdPrefix = str =>
8+ str.slice(0, 2) === 'm/' ? str.slice(2) : str;
9+export const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
810
9−const ledgerSignTransaction = async (txn, hdpath) => {
11+export const ledgerSignTransaction = async (txn, hdpath) => {
1012 const transport = await Transport.create();
1113 const eth = new Eth(transport);
1214
@@ -20,5 +22,3 @@ const ledgerSignTransaction = async (txn, hdpath) => {
2022
2123 return txn;
2224};
23−
24−export { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH, ledgerSignTransaction };
src/views/Activate/ActivateCode.js+1−2
@@ -36,12 +36,11 @@ export default function ActivateCode() {
3636 setDerivedPoint,
3737 } = useActivateFlow();
3838
39− const ticketInput = useTicketInput({
39+ const [ticketInput, { pass: validTicket }] = useTicketInput({
4040 name: 'ticket',
4141 label: 'Code',
4242 autoFocus: true,
4343 });
44− const validTicket = ticketInput.pass;
4544
4645 const goToLogin = useCallback(() => history.popAndPush(ROUTE_NAMES.LOGIN), [
4746 history,
src/views/Activate/ActivateDisclaimer.js+3−3
@@ -12,7 +12,7 @@ const TEXT_STYLE = 'f5';
1212
1313export default function ActivateDisclaimer() {
1414 const { push, names } = useLocalRouter();
15− const checkboxInput = useCheckboxInput({
15+ const [understoodInput, { data: isUnderstood }] = useCheckboxInput({
1616 name: 'checkbox',
1717 label: 'I acknowledge and understand these rights',
1818 });
@@ -70,10 +70,10 @@ export default function ActivateDisclaimer() {
7070 <Grid.Item full as={WarningBox}>
7171 Warning: Nobody but you can restore or reset your Master Ticket
7272 </Grid.Item>
73− <Grid.Item as={CheckboxInput} {...checkboxInput} full />
73+ <Grid.Item as={CheckboxInput} {...understoodInput} full />
7474 <Grid.Item
7575 as={ForwardButton}
76− disabled={!checkboxInput.data}
76+ disabled={!isUnderstood}
7777 onClick={goToPassport}
7878 solid
7979 full>
src/views/Activate/PassportVerify.js+1−2
@@ -24,14 +24,13 @@ export default function PassportVerify({ className }) {
2424 () => [validateExactly(ticket, 'Does not match expected master ticket.')],
2525 [ticket]
2626 );
27− const ticketInput = useTicketInput({
27+ const [ticketInput, { pass }] = useTicketInput({
2828 name: 'ticket',
2929 label: 'Master Ticket',
3030 initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
3131 autoFocus: true,
3232 validators,
3333 });
34− const { pass } = ticketInput;
3534
3635 return (
3736 <Grid className={cn(className, 'auto-rows-min')}>
src/views/Login/Keystore.js+1−2
@@ -23,12 +23,11 @@ export default function Keystore({ className }) {
2323 // inputs
2424 // keystore: Maybe<String>
2525 const [keystore, setKeystore] = useState(Maybe.Nothing());
26− const passphraseInput = usePassphraseInput({
26+ const [passphraseInput, { data: passphrase }] = usePassphraseInput({
2727 name: 'password',
2828 label: 'Keystore password',
2929 autoFocus: true,
3030 });
31− const passphrase = passphraseInput.data;
3231
3332 const constructWallet = () => {
3433 try {
src/views/Login/Ledger.js+102−87
@@ -1,6 +1,6 @@
1−import React, { useState } from 'react';
1+import React, { useCallback, useEffect, useState } from 'react';
22import Maybe from 'folktale/maybe';
3−import { P, Input, Grid, H4, H5 } from 'indigo-react';
3+import { P, Text, Input, Grid, H5, CheckboxInput } from 'indigo-react';
44import { times } from 'lodash';
55import * as bip32 from 'bip32';
66import Transport from '@ledgerhq/hw-transport-u2f';
@@ -9,17 +9,22 @@ import * as secp256k1 from 'secp256k1';
99
1010import { ForwardButton } from 'components/Buttons';
1111import { InnerLabelDropdown } from 'components/old/Base';
12+import { useCheckboxInput, useHdPathInput } from 'components/Inputs';
1213
1314import { useWallet } from 'store/wallet';
1415
15−import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
16+import {
17+ LEDGER_LIVE_PATH,
18+ LEDGER_LEGACY_PATH,
19+ chopHdPrefix,
20+ addHdPrefix,
21+} from 'lib/ledger';
1622import { WALLET_TYPES } from 'lib/wallet';
1723import useLoginView from 'lib/useLoginView';
1824
1925const pathOptions = [
2026 { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
2127 { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
22− { title: 'Custom path', value: 'custom' },
2328];
2429
2530const accountOptions = times(20, i => ({
@@ -27,58 +32,70 @@ const accountOptions = times(20, i => ({
2732 value: i,
2833}));
2934
30−const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
31−const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
32−
3335export default function Ledger({ className }) {
3436 useLoginView(WALLET_TYPES.LEDGER);
3537
3638 const { setWallet, setWalletHdPath } = useWallet();
3739
38− const [basePath, setBasePath] = useState(LEDGER_LIVE_PATH);
39− const [account, setAccount] = useState(0);
40− const [hdPath, setHdPath] = useState(LEDGER_LIVE_PATH.replace(/x/g, 0));
41−
42− const updateHdPath = (basePath, account) => {
43− if (basePath !== 'custom') {
44− setHdPath(basePath.replace(/x/g, account));
45− }
46− };
47−
48− const handlePathSelection = basePath => {
49− setBasePath(basePath);
50− updateHdPath(basePath, account);
51− };
52−
53− const handleAccountSelection = account => {
54− setAccount(account);
55− updateHdPath(basePath, account);
56− };
57−
58− const pollDevice = async () => {
40+ // ledger has two derivation path _patterns_
41+ const [basePathPattern, setBasePathPattern] = useState(LEDGER_LIVE_PATH);
42+ // and then we want to be able to select an index within that path
43+ const [accountIndex, setAccountIndex] = useState(0);
44+
45+ // custom toggle
46+ const [customPathInput, { data: useCustomPath }] = useCheckboxInput({
47+ name: 'customPath',
48+ label: 'Custom HD Path',
49+ autoComplete: 'off',
50+ initialValue: false,
51+ });
52+
53+ // hd path input
54+ const [
55+ hdPathInput,
56+ { data: hdPath },
57+ { setValue: setHdPath },
58+ ] = useHdPathInput({
59+ name: 'hdpath',
60+ label: 'HD Path',
61+ initialValue: basePathPattern.replace(/x/g, 0),
62+ });
63+
64+ const pollDevice = useCallback(async () => {
5965 const transport = await Transport.create();
6066 const eth = new Eth(transport);
6167 const path = chopHdPrefix(hdPath);
6268
63− eth.getAddress(path, false, true).then(
64− info => {
65− const publicKey = Buffer.from(info.publicKey, 'hex');
66− const chainCode = Buffer.from(info.chainCode, 'hex');
67− const pub = secp256k1.publicKeyConvert(publicKey, true);
68− const hd = bip32.fromPublicKey(pub, chainCode);
69− setWallet(Maybe.Just(hd));
70− setWalletHdPath(addHdPrefix(hdPath));
71− },
72− _ => {
73− setWallet(Maybe.Nothing());
74− }
75− );
76− };
77−
78− const basePathTitle = pathOptions.find(o => o.value === basePath).title;
79− const accountTitle = accountOptions.find(o => o.value === account).title;
80−
81− const isHTTPS = document.location.protocol === 'https:';
69+ try {
70+ const info = await eth.getAddress(path, false, true);
71+ const publicKey = Buffer.from(info.publicKey, 'hex');
72+ const chainCode = Buffer.from(info.chainCode, 'hex');
73+ const pub = secp256k1.publicKeyConvert(publicKey, true);
74+ const hd = bip32.fromPublicKey(pub, chainCode);
75+ setWallet(Maybe.Just(hd));
76+ setWalletHdPath(addHdPrefix(hdPath));
77+ } catch (error) {
78+ console.error(error);
79+ setWallet(Maybe.Nothing());
80+ }
81+ }, [hdPath, setWallet, setWalletHdPath]);
82+
83+ // when the base path pattern or the account index changes
84+ // update the hd path in our input
85+ useEffect(() => {
86+ if (useCustomPath) {
87+ // updated by useForm
88+ } else {
89+ setHdPath(basePathPattern.replace(/x/g, accountIndex));
90+ }
91+ }, [useCustomPath, setHdPath, basePathPattern, accountIndex]);
92+
93+ const basePathPatternTitle = pathOptions.find(
94+ o => o.value === basePathPattern
95+ ).title;
96+ const accountTitle = accountOptions.find(o => o.value === accountIndex).title;
97+
98+ const isHTTPS = document.location.protocol === 'http:';
8299
83100 // when not on https, tell user how to get there
84101 const renderHTTP = () => (
@@ -114,67 +131,65 @@ export default function Ledger({ className }) {
114131 <li className="mt3">
115132 Run <code>python bridge-https.py</code>
116133 </li>
134+ <li className="mt3">Return to this page.</li>
117135 </Grid.Item>
118136 </>
119137 );
120138
121139 const renderHTTPS = () => (
122140 <>
123− <Grid.Item full as={P}>
141+ <Grid.Item full as={H5}>
142+ Authenticate With Your Ledger
143+ </Grid.Item>
144+ <Grid.Item full as={Text} className="f6">
124145 Connect and authenticate to your Ledger, and then open the "Ethereum"
125146 application. If you're running on older firmware, make sure the "browser
126147 support" option is turned on. To sign transactions, you'll also need to
127148 enable the "contract data" option.
128149 </Grid.Item>
129150
130− <Grid.Item full as={P}>
131− If you'd like to use a custom derivation path, you may enter it below.
132− </Grid.Item>
151+ {useCustomPath && (
152+ <Grid.Item full as={Input} className="mv3" {...hdPathInput} />
153+ )}
133154
134− <Grid.Item
135− full
136− as={InnerLabelDropdown}
137− className="mv4"
138− title="Derivation path"
139− options={pathOptions}
140− handleUpdate={handlePathSelection}
141− currentSelectionTitle={basePathTitle}
142− />
143−
144− {basePath === 'custom' ? (
145− <Grid.Item
146− full
147− as={Input}
148− className="mv3"
149− name="hdPath"
150− label="HD path"
151− autoComplete="off"
152− initialValue={addHdPrefix(hdPath)}
153− onValue={setHdPath}
154− />
155− ) : (
156− <Grid.Item
157− full
158− as={InnerLabelDropdown}
159− className="mt4"
160− title="Account"
161− options={accountOptions}
162− handleUpdate={handleAccountSelection}
163− currentSelectionTitle={accountTitle}
164− />
155+ {!useCustomPath && (
156+ <>
157+ <Grid.Item
158+ full
159+ as={InnerLabelDropdown}
160+ className="mv4"
161+ title="Derivation path"
162+ options={pathOptions}
163+ handleUpdate={setBasePathPattern}
164+ currentSelectionTitle={basePathPatternTitle}
165+ />
166+ <Grid.Item
167+ full
168+ as={InnerLabelDropdown}
169+ className="mt4"
170+ title="Account"
171+ options={accountOptions}
172+ handleUpdate={setAccountIndex}
173+ currentSelectionTitle={accountTitle}
174+ />
175+ </>
165176 )}
166177
167− <ForwardButton className="mt3" onClick={pollDevice}>
178+ <Grid.Item full as={CheckboxInput} {...customPathInput} />
179+
180+ <Grid.Item
181+ full
182+ as={ForwardButton}
183+ solid
184+ className="mt3"
185+ onClick={pollDevice}>
168186 Authenticate
169− </ForwardButton>
187+ </Grid.Item>
170188 </>
171189 );
172190
173191 return (
174192 <Grid className={className}>
175− <Grid.Item full as={H4}>
176− Authenticate With Your Ledger
177− </Grid.Item>
178193 {isHTTPS && renderHTTPS()}
179194 {!isHTTPS && renderHTTP()}
180195 </Grid>
src/views/Login/Mnemonic.js+25−27
@@ -1,5 +1,5 @@
1−import React, { useCallback, useEffect } from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
1+import React, { useEffect } from 'react';
2+import Maybe from 'folktale/maybe';
33import { Grid, Input, CheckboxInput } from 'indigo-react';
44
55import {
@@ -19,61 +19,59 @@ export default function Mnemonic({ className }) {
1919
2020 const {
2121 setWallet,
22− authMnemonic,
2322 setAuthMnemonic,
2423 walletHdPath,
2524 setWalletHdPath,
2625 } = useWallet();
2726
28− const advancedInput = useCheckboxInput({
27+ const [advancedInput, { data: useAdvanced }] = useCheckboxInput({
2928 name: 'advanced',
3029 label: 'Passphrase & HD Path',
3130 initialValue: false,
3231 });
33− const mnemonic = authMnemonic.getOrElse('');
34− // TODO: move this into transformers?
35− // transform the result of the mnemonic to Maybe<string>
36− const _setAuthMnemonic = useCallback(
37− mnemonic => setAuthMnemonic(mnemonic === '' ? Nothing() : Just(mnemonic)),
38− [setAuthMnemonic]
39− );
4032
41− const mnemonicInput = useMnemonicInput({
33+ const [mnemonicInput, { pass, data: mnemonic }] = useMnemonicInput({
4234 name: 'mnemonic',
4335 label: 'BIP39 Mnemonic',
44− initialValue: mnemonic,
45− onValue: _setAuthMnemonic,
4636 autoFocus: true,
4737 });
4838
49− const passphraseInput = usePassphraseInput({
39+ const [passphraseInput, { data: passphrase }] = usePassphraseInput({
5040 name: 'passphrase',
5141 label: '(Optional) Wallet Passphrase',
5242 });
53− const passphrase = passphraseInput.data;
5443
55− const hdPathInput = useHdPathInput({
44+ const [hdPathInput, { data: hdPath }] = useHdPathInput({
5645 name: 'hdpath',
5746 label: 'HD Path',
5847 initialValue: walletHdPath,
59− onValue: setWalletHdPath,
6048 });
6149
62− // when the properties change, re-derive wallet
50+ // when the properties change, re-derive wallet and set global state
6351 useEffect(() => {
64− let mounted = true;
65− (async () => {
66− const wallet = walletFromMnemonic(mnemonic, walletHdPath, passphrase);
67− mounted && setWallet(wallet);
68− })();
69− return () => (mounted = false);
70− }, [mnemonic, passphrase, walletHdPath, setWallet]);
52+ if (pass) {
53+ setWalletHdPath(hdPath);
54+ setAuthMnemonic(Maybe.Just(mnemonic));
55+ setWallet(walletFromMnemonic(mnemonic, hdPath, passphrase));
56+ } else {
57+ setAuthMnemonic(Maybe.Nothing());
58+ setWallet(Maybe.Nothing());
59+ }
60+ }, [
61+ pass,
62+ mnemonic,
63+ passphrase,
64+ hdPath,
65+ setWallet,
66+ setAuthMnemonic,
67+ setWalletHdPath,
68+ ]);
7169
7270 return (
7371 <Grid className={className}>
7472 <Grid.Item full as={Input} {...mnemonicInput} />
7573
76− {advancedInput.data && (
74+ {useAdvanced && (
7775 <>
7876 <Grid.Item full as={Input} {...passphraseInput} />
7977 <Grid.Item full as={Input} {...hdPathInput} />
src/views/Login/PrivateKey.js+1−2
@@ -14,13 +14,12 @@ export default function PrivateKey({ className }) {
1414
1515 const { setWallet } = useWallet();
1616
17− const privateKeyInput = useHexInput({
17+ const [privateKeyInput, { pass, data: privateKey }] = useHexInput({
1818 length: 64,
1919 name: 'privateKey',
2020 label: 'Private key',
2121 autoFocus: true,
2222 });
23− const { pass, data: privateKey } = privateKeyInput;
2423
2524 useEffect(() => {
2625 if (pass) {
src/views/Login/Shards.jsdeleted−96
@@ -1,96 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
3−import * as kg from 'urbit-key-generation/dist/index';
4−import { Input } from 'indigo-react';
5−
6−import View from 'components/View';
7−import {
8− useTicketInput,
9− usePassphraseInput,
10− usePointInput,
11−} from 'components/Inputs';
12−import { ForwardButton } from 'components/Buttons';
13−
14−import { useWallet } from 'store/wallet';
15−
16−import { urbitWalletFromTicket, WALLET_TYPES } from 'lib/wallet';
17−import useLoginView from 'lib/useLoginView';
18−
19−export default function Shards({ loginCompleted }) {
20− useLoginView(WALLET_TYPES.SHARDS);
21−
22− const { wallet, setUrbitWallet } = useWallet();
23−
24− const pointInput = usePointInput({
25− name: 'point',
26− autoFocus: true,
27− });
28− const pointName = pointInput.data;
29− const shard1Input = useTicketInput({
30− name: 'shard1',
31− label: 'Shard 1',
32− });
33− const shard1 = shard1Input.data;
34− const shard2Input = useTicketInput({
35− name: 'shard2',
36− label: 'Shard 2',
37− });
38− const shard2 = shard2Input.data;
39− const shard3Input = useTicketInput({
40− name: 'shard3',
41− label: 'Shard 3',
42− });
43− const shard3 = shard3Input.data;
44−
45− const passphraseInput = usePassphraseInput({
46− name: 'passphrase',
47− label: '(Optional) Wallet Passphrase',
48− });
49− const passphrase = passphraseInput.data;
50−
51− const walletFromShards = async () => {
52− const s1 = shard1 === '' ? undefined : shard1;
53− const s2 = shard2 === '' ? undefined : shard2;
54− const s3 = shard3 === '' ? undefined : shard3;
55−
56− let ticket = undefined;
57− try {
58− ticket = kg.combine([s1, s2, s3]);
59− } catch (_) {
60− // do nothing
61− }
62−
63− if (ticket !== undefined) {
64− const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
65− setUrbitWallet(Just(uhdw));
66− }
67− };
68−
69− const shards = [shard1, shard2, shard3];
70− const ready = shards.filter(x => x !== '').length > 1;
71−
72− return (
73− <>
74− Enter your point and at least two of your three Urbit master ticket shards
75− here. The index of the input field needs to match the index of the shard.
76− <Input {...pointInput} />
77− <Input {...shard1Input} />
78− <Input {...shard2Input} />
79− <Input {...shard3Input} />
80− If your wallet requires a passphrase, you may enter it below.
81− <Input {...passphraseInput} />
82− <ForwardButton
83− className="mt3"
84− disabled={!ready}
85− onClick={walletFromShards}>
86− Unlock Wallet
87− </ForwardButton>
88− <ForwardButton
89− className="mt3"
90− disabled={Nothing.hasInstance(wallet)}
91− onClick={loginCompleted}>
92− Continue
93− </ForwardButton>
94− </>
95− );
96−}
src/views/Login/Ticket.js+23−42
@@ -23,30 +23,26 @@ import useLoginView from 'lib/useLoginView';
2323
2424export default function Ticket({ className }) {
2525 useLoginView(WALLET_TYPES.TICKET);
26− // globals
26+
2727 const { contracts } = useNetwork();
2828 const { setUrbitWallet } = useWallet();
2929 const { setPointCursor } = usePointCursor();
3030 const impliedPoint = useImpliedPoint();
3131
32− // inputs
33−
3432 // point
35− const pointInput = usePointInput({
33+ const [pointInput, { data: pointName }] = usePointInput({
3634 name: 'point',
3735 initialValue: impliedPoint,
3836 autoFocus: true,
3937 });
40− const pointName = pointInput.data;
4138
4239 // passphrase
43− const passphraseInput = usePassphraseInput({
40+ const [passphraseInput, { data: passphrase }] = usePassphraseInput({
4441 name: 'passphrase',
45− label: '(Optional) Wallet Passphrase',
42+ label: 'Wallet Passphrase',
4643 });
47− const passphrase = passphraseInput.data;
4844
49− const hasPassphraseInput = useCheckboxInput({
45+ const [hasPassphraseInput] = useCheckboxInput({
5046 name: 'has-passphrase',
5147 label: 'Passphrase',
5248 initialValue: false,
@@ -55,40 +51,36 @@ export default function Ticket({ className }) {
5551 // ticket
5652 const [error, setError] = useState();
5753 const [deriving, setDeriving] = useState(false);
58− const ticketInput = useTicketInput({
54+ const [ticketInput, { data: ticket, pass: validTicket }] = useTicketInput({
5955 name: 'ticket',
6056 label: 'Master Ticket',
6157 error,
6258 deriving,
6359 });
64− const ticket = ticketInput.data;
6560
6661 // shards
67− const shardsInput = useCheckboxInput({
62+ const [shardsInput, { data: isUsingShards }] = useCheckboxInput({
6863 name: 'shards',
6964 label: 'Shards',
7065 initialValue: false,
7166 });
7267
73− const shard1Input = useTicketInput({
68+ const [shard1Input, { data: shard1, pass: shard1Pass }] = useTicketInput({
7469 name: 'shard1',
7570 label: 'Shard 1',
7671 });
77− const shard1 = shard1Input.data;
78− const shard2Input = useTicketInput({
72+
73+ const [shard2Input, { data: shard2, pass: shard2Pass }] = useTicketInput({
7974 name: 'shard2',
8075 label: 'Shard 2',
8176 });
82− const shard2 = shard2Input.data;
83− const shard3Input = useTicketInput({
77+
78+ const [shard3Input, { data: shard3, pass: shard3Pass }] = useTicketInput({
8479 name: 'shard3',
8580 label: 'Shard 3',
8681 });
87− const shard3 = shard3Input.data;
8882
89− const isUsingShards = !!shardsInput.data;
90− const shards = [shard1, shard2, shard3];
91− const shardsReady = shards.filter(x => x !== '').length > 1;
83+ const shardsReady = shard1Pass && shard2Pass && shard3Pass;
9284
9385 // TODO: maybe want to do this only on-go, because wallet derivation is slow...
9486 const deriveWalletFromTicket = useCallback(async () => {
@@ -144,10 +136,6 @@ export default function Ticket({ className }) {
144136 ]);
145137
146138 const deriveWalletFromShards = useCallback(async () => {
147− if (!shardsReady) {
148− return;
149− }
150−
151139 const s1 = shard1 || undefined;
152140 const s2 = shard2 || undefined;
153141 const s3 = shard3 || undefined;
@@ -159,29 +147,22 @@ export default function Ticket({ className }) {
159147 } catch (_) {
160148 // do nothing
161149 }
162− }, [
163− passphrase,
164− pointName,
165− shardsReady,
166− setUrbitWallet,
167− shard1,
168− shard2,
169− shard3,
170− ]);
150+ }, [passphrase, pointName, setUrbitWallet, shard1, shard2, shard3]);
171151
172− // use shards if
152+ // derive wallet on change
173153 useEffect(() => {
174154 if (isUsingShards && shardsReady) {
175155 deriveWalletFromShards();
176− }
177− }, [isUsingShards, shardsReady, deriveWalletFromShards]);
178−
179− // verify ticket if not using shards
180− useEffect(() => {
181− if (!isUsingShards) {
156+ } else if (validTicket) {
182157 deriveWalletFromTicket();
183158 }
184− }, [isUsingShards, deriveWalletFromTicket]);
159+ }, [
160+ isUsingShards,
161+ validTicket,
162+ shardsReady,
163+ deriveWalletFromShards,
164+ deriveWalletFromTicket,
165+ ]);
185166
186167 return (
187168 <Grid className={className}>
src/views/Login/Trezor.js+65−47
@@ -1,10 +1,10 @@
1−import * as bip32 from 'bip32';
2−import React, { useState } from 'react';
1+import React, { useEffect, useState } from 'react';
32import Maybe from 'folktale/maybe';
3+import * as bip32 from 'bip32';
44import { times } from 'lodash';
55import TrezorConnect from 'trezor-connect';
66import * as secp256k1 from 'secp256k1';
7−import { P, Input, Grid, H5 } from 'indigo-react';
7+import { Text, Input, Grid, H5, CheckboxInput } from 'indigo-react';
88
99import { ForwardButton } from 'components/Buttons';
1010import { InnerLabelDropdown } from 'components/old/Base';
@@ -14,26 +14,39 @@ import { WALLET_TYPES } from 'lib/wallet';
1414
1515import { useWallet } from 'store/wallet';
1616import useLoginView from 'lib/useLoginView';
17+import { useHdPathInput, useCheckboxInput } from 'components/Inputs';
1718
18−const accountOptions = [
19− { title: 'Custom path', value: 'custom' },
20− ...times(20, i => ({ title: `Account #${i + 1}`, value: i })),
21−];
19+const accountOptions = times(20, i => ({
20+ title: `Account #${i + 1}`,
21+ value: i,
22+}));
2223
24+// see Ledger.js for context — Trezor is basicaly Ledger with less complexity
2325export default function Trezor({ className }) {
2426 useLoginView(WALLET_TYPES.TREZOR);
2527
2628 const { setWallet, setWalletHdPath } = useWallet();
2729
28− const [hdPath, setHdPath] = useState(TREZOR_PATH.replace(/x/g, 0));
29− const [account, setAccount] = useState(0);
30−
31− const handleAccountSelection = account => {
32− setAccount(account);
33− if (account !== 'custom') {
34− setHdPath(TREZOR_PATH.replace(/x/g, account));
35− }
36− };
30+ const [accountIndex, setAccountIndex] = useState(0);
31+
32+ // custom toggle
33+ const [customPathInput, { data: useCustomPath }] = useCheckboxInput({
34+ name: 'customPath',
35+ label: 'Custom HD Path',
36+ autoComplete: 'off',
37+ initialValue: false,
38+ });
39+
40+ // hd path input
41+ const [
42+ hdPathInput,
43+ { data: hdPath },
44+ { setValue: setHdPath },
45+ ] = useHdPathInput({
46+ name: 'hdpath',
47+ label: 'HD Path',
48+ initialValue: TREZOR_PATH.replace(/x/g, 0),
49+ });
3750
3851 const pollDevice = async () => {
3952 TrezorConnect.manifest({
@@ -41,22 +54,32 @@ export default function Trezor({ className }) {
4154 appUrl: 'https://github.com/urbit/bridge',
4255 });
4356
44− TrezorConnect.getPublicKey({ path: hdPath }).then(info => {
45− if (info.success === true) {
46− const payload = info.payload;
47− const publicKey = Buffer.from(payload.publicKey, 'hex');
48− const chainCode = Buffer.from(payload.chainCode, 'hex');
49− const pub = secp256k1.publicKeyConvert(publicKey, true);
50− const hd = bip32.fromPublicKey(pub, chainCode);
51− setWallet(Maybe.Just(hd));
52− setWalletHdPath(hdPath);
53− } else {
54− setWallet(Maybe.Nothing());
55− }
57+ const info = await TrezorConnect.getPublicKey({
58+ path: hdPath,
5659 });
60+
61+ if (info.success === true) {
62+ const payload = info.payload;
63+ const publicKey = Buffer.from(payload.publicKey, 'hex');
64+ const chainCode = Buffer.from(payload.chainCode, 'hex');
65+ const pub = secp256k1.publicKeyConvert(publicKey, true);
66+ const hd = bip32.fromPublicKey(pub, chainCode);
67+ setWallet(Maybe.Just(hd));
68+ setWalletHdPath(hdPath);
69+ } else {
70+ setWallet(Maybe.Nothing());
71+ }
5772 };
5873
59− const accountTitle = accountOptions.find(o => o.value === account).title;
74+ useEffect(() => {
75+ if (useCustomPath) {
76+ // updated by useForm
77+ } else {
78+ setHdPath(TREZOR_PATH.replace(/x/g, accountIndex));
79+ }
80+ }, [useCustomPath, setHdPath, accountIndex]);
81+
82+ const accountTitle = accountOptions.find(o => o.value === accountIndex).title;
6083
6184 return (
6285 <Grid className={className}>
@@ -64,34 +87,29 @@ export default function Trezor({ className }) {
6487 Authenticate With Your Trezor
6588 </Grid.Item>
6689
67− <Grid.Item full as={P}>
90+ <Grid.Item full as={Text} className="f6">
6891 Connect and authenticate to your Trezor. If you'd like to use a custom
6992 derivation path, you may enter it below.
7093 </Grid.Item>
7194
72− <Grid.Item
73− full
74− as={InnerLabelDropdown}
75− className="mv4"
76− title="Account"
77− options={accountOptions}
78− handleUpdate={handleAccountSelection}
79− currentSelectionTitle={accountTitle}
80− />
81−
82− {account === 'custom' && (
95+ {useCustomPath && (
96+ <Grid.Item full as={Input} className="mv3" {...hdPathInput} />
97+ )}
98+
99+ {!useCustomPath && (
83100 <Grid.Item
84101 full
85− as={Input}
102+ as={InnerLabelDropdown}
86103 className="mv4"
87− name="hdPath"
88− label="HD path"
89− autoComplete="off"
90− initialValue={hdPath}
91− onValue={setHdPath}
104+ title="Account"
105+ options={accountOptions}
106+ handleUpdate={setAccountIndex}
107+ currentSelectionTitle={accountTitle}
92108 />
93109 )}
94110
111+ <Grid.Item full as={CheckboxInput} {...customPathInput} />
112+
95113 <Grid.Item
96114 full
97115 as={ForwardButton}
src/views/ViewPoint.js+1−2
@@ -17,11 +17,10 @@ import { usePointInput } from 'components/Inputs';
1717export default function ViewPoint() {
1818 const history = useHistory();
1919 const { setPointCursor } = usePointCursor();
20− const pointInput = usePointInput({
20+ const [pointInput, { data: pointName, pass }] = usePointInput({
2121 name: 'point',
2222 autoFocus: true,
2323 });
24− const { data: pointName, pass } = pointInput;
2524
2625 const disabled = !pass;
2726 const goForward = useCallback(() => {