on master help sign in
feat: improve useForm memoization, update useFirstOf api, clean up Ledger/Trezor views
cd91628Matt Condon (shrugs)
src/components/Footer.js+2−1
@@ -1,8 +1,9 @@
1+import React from 'react';
1import { createTeleporter } from 'react-teleporter';
2import { createTeleporter } from 'react-teleporter';
2
3
3const FooterTeleporter = createTeleporter();
4const FooterTeleporter = createTeleporter();
4
5
5const Footer = FooterTeleporter.Source;
6const Footer = FooterTeleporter.Source;
6−Footer.Target = FooterTeleporter.Target;
7+Footer.Target = props => <FooterTeleporter.Target as="footer" {...props} />;
7
8
8export default Footer;
9export 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';
2import { AccessoryIcon, useForm } from 'indigo-react';
2import { AccessoryIcon, useForm } from 'indigo-react';
3+import { identity } from 'lodash';
3import * as bip39 from 'bip39';
4import * as bip39 from 'bip39';
4
5
5import InputSigil from 'components/InputSigil';
6import InputSigil from 'components/InputSigil';
@@ -45,10 +46,35 @@ const EXAMPLE_PRIVATE_KEY =
45// transformers: [prependSig],
46// transformers: [prependSig],
46// });
47// });
47
48
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+}
49
75
50export function usePassphraseInput(props) {
76export function usePassphraseInput(props) {
51− return firstOf(
77+ return useFirstOf(
52 useForm([
78 useForm([
53 {
79 {
54 type: 'password',
80 type: 'password',
@@ -61,7 +87,7 @@ export function usePassphraseInput(props) {
61}
87}
62
88
63export function useHexInput({ length, ...rest }) {
89export function useHexInput({ length, ...rest }) {
64− return firstOf(
90+ return useFirstOf(
65 useForm([
91 useForm([
66 {
92 {
67 type: 'text', // or password
93 type: 'text', // or password
@@ -84,7 +110,7 @@ export function useHexInput({ length, ...rest }) {
84const kExampleMnemonic = bip39.generateMnemonic();
110const kExampleMnemonic = bip39.generateMnemonic();
85const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
111const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
86export function useMnemonicInput(props) {
112export function useMnemonicInput(props) {
87− return firstOf(
113+ return useFirstOf(
88 useForm([
114 useForm([
89 {
115 {
90 type: 'text',
116 type: 'text',
@@ -98,7 +124,7 @@ export function useMnemonicInput(props) {
98}
124}
99
125
100export function useHdPathInput(props) {
126export function useHdPathInput(props) {
101− return firstOf(
127+ return useFirstOf(
102 useForm([
128 useForm([
103 {
129 {
104 type: 'text',
130 type: 'text',
@@ -114,7 +140,7 @@ const kTicketValidators = [validateTicket, validateNotEmpty];
114const kTicketTransformers = [prependSig];
140const kTicketTransformers = [prependSig];
115//TODO needs to be fancier, displaying sig and dashes instead of •ing all
141//TODO needs to be fancier, displaying sig and dashes instead of •ing all
116export function useTicketInput({ validators = [], deriving = false, ...rest }) {
142export function useTicketInput({ validators = [], deriving = false, ...rest }) {
117− const input = firstOf(
143+ return useFirstOf(
118 useForm([
144 useForm([
119 {
145 {
120 type: 'password',
146 type: 'password',
@@ -127,28 +153,24 @@ export function useTicketInput({ validators = [], deriving = false, ...rest }) {
127 mono: true,
153 mono: true,
128 ...rest,
154 ...rest,
129 },
155 },
130− ])
156+ ]),
157+ ({ error, pass }) => ({
158+ accessory: error ? (
159+ <AccessoryIcon.Failure />
160+ ) : deriving ? (
161+ <AccessoryIcon.Pending />
162+ ) : pass ? (
163+ <AccessoryIcon.Success />
164+ ) : null,
165+ })
131 );
166 );
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− };
145}
167}
146
168
147const kPointValidators = [validatePoint, validateNotEmpty];
169const kPointValidators = [validatePoint, validateNotEmpty];
148const kPointTransformers = [prependSig];
170const kPointTransformers = [prependSig];
149export function usePointInput(rest) {
171export function usePointInput(rest) {
150 const [lastValidPoint, setLastValidPoint] = useState('');
172 const [lastValidPoint, setLastValidPoint] = useState('');
151− const input = firstOf(
173+ return useFirstOf(
152 useForm([
174 useForm([
153 {
175 {
154 type: 'text',
176 type: 'text',
@@ -160,28 +182,24 @@ export function usePointInput(rest) {
160 onValue: setLastValidPoint,
182 onValue: setLastValidPoint,
161 ...rest,
183 ...rest,
162 },
184 },
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+ })
164 );
198 );
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− };
181}
199}
182
200
183export function useCheckboxInput({ initialValue, ...rest }) {
201export function useCheckboxInput({ initialValue, ...rest }) {
184− return firstOf(
202+ return useFirstOf(
185 useForm([
203 useForm([
186 {
204 {
187 type: 'checkbox',
205 type: 'checkbox',
src/indigo-react/components/CheckboxInput.js+1
@@ -31,6 +31,7 @@ export default function CheckboxInput({
31 initialValue,
31 initialValue,
32 validators,
32 validators,
33 transformers,
33 transformers,
34+ setValue,
34
35
35 // extra
36 // extra
36 ...rest
37 ...rest
src/indigo-react/components/Input.js+1
@@ -33,6 +33,7 @@ export default function Input({
33 initialValue,
33 initialValue,
34 validators,
34 validators,
35 transformers,
35 transformers,
36+ setValue,
36
37
37 // extra
38 // extra
38 ...rest
39 ...rest
src/indigo-react/lib/useForm.js+21−9
@@ -1,9 +1,10 @@
1import { useCallback, useMemo, useState } from 'react';
1import { useCallback, useMemo, useState } from 'react';
2−import { keyBy, get, every, some } from 'lodash';
2+import { isEqual, keyBy, get, every, some } from 'lodash';
3
3
4import { compose } from 'lib/lib';
4import { compose } from 'lib/lib';
5import { kDefaultValidator } from 'lib/validators';
5import { kDefaultValidator } from 'lib/validators';
6import useSetState from 'lib/useSetState';
6import useSetState from 'lib/useSetState';
7+import usePreviousValue from 'lib/usePreviousValue';
7
8
8// interface InputConfig {
9// interface InputConfig {
9// name: string;
10// name: string;
@@ -24,7 +25,15 @@ const defaultsFor = (configs, mapper) =>
24/**
25/**
25 * useForm manages a set of inputs for rendering them in a loop
26 * useForm manages a set of inputs for rendering them in a loop
26 */
27 */
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+
28 const byName = useMemo(() => keyBy(configs, 'name'), [configs]);
37 const byName = useMemo(() => keyBy(configs, 'name'), [configs]);
29
38
30 // track values
39 // track values
@@ -38,11 +47,6 @@ export default function useForm(configs = []) {
38 // but for dynamically added input configs, we won't get validation, etc
47 // but for dynamically added input configs, we won't get validation, etc
39 // until there's a traditional state update.
48 // until there's a traditional state update.
40
49
41− const setValue = useCallback(
42− (name, value) => _setValues(values => ({ ...values, [name]: value })),
43− [_setValues]
44− );
45−
46 // track focused states
50 // track focused states
47 const [focused, setFocused] = useSetState(() =>
51 const [focused, setFocused] = useSetState(() =>
48 defaultsFor(configs, config => config.autoFocus && !config.disabled)
52 defaultsFor(configs, config => config.autoFocus && !config.disabled)
@@ -61,6 +65,13 @@ export default function useForm(configs = []) {
61 [byName]
65 [byName]
62 );
66 );
63
67
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+
64 // build fn that validates a value by input name
75 // build fn that validates a value by input name
65 const validate = useCallback(
76 const validate = useCallback(
66 (name, value) =>
77 (name, value) =>
@@ -84,8 +95,8 @@ export default function useForm(configs = []) {
84
95
85 // on change, transform and set value
96 // on change, transform and set value
86 const onChange = useCallback(
97 const onChange = useCallback(
87− name => e => setValue(name, transform(name, getValue(name, e))), //
98+ name => e => setValue(name, getValue(name, e)), //
88− [setValue, transform, getValue]
99+ [setValue, getValue]
89 );
100 );
90
101
91 // on focus, update focus
102 // on focus, update focus
@@ -198,5 +209,6 @@ export default function useForm(configs = []) {
198 inputs,
209 inputs,
199 pass,
210 pass,
200 error,
211 error,
212+ setValue,
201 };
213 };
202}
214}
src/lib/ledger.js+6−6
@@ -1,12 +1,14 @@
1import Transport from '@ledgerhq/hw-transport-u2f';
1import Transport from '@ledgerhq/hw-transport-u2f';
2import Eth from '@ledgerhq/hw-app-eth';
2import Eth from '@ledgerhq/hw-app-eth';
3
3
4−const LEDGER_LEGACY_PATH = "m/44'/60'/0'/x";
4+export const LEDGER_LEGACY_PATH = "m/44'/60'/0'/x";
5−const LEDGER_LIVE_PATH = "m/44'/60'/x'/0/0";
5+export const LEDGER_LIVE_PATH = "m/44'/60'/x'/0/0";
6
6
7+export const chopHdPrefix = str =>
7−const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
8+ str.slice(0, 2) === 'm/' ? str.slice(2) : str;
9+export const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
8
10
9−const ledgerSignTransaction = async (txn, hdpath) => {
11+export const ledgerSignTransaction = async (txn, hdpath) => {
10 const transport = await Transport.create();
12 const transport = await Transport.create();
11 const eth = new Eth(transport);
13 const eth = new Eth(transport);
12
14
@@ -20,5 +22,3 @@ const ledgerSignTransaction = async (txn, hdpath) => {
20
22
21 return txn;
23 return txn;
22};
24};
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() {
36 setDerivedPoint,
36 setDerivedPoint,
37 } = useActivateFlow();
37 } = useActivateFlow();
38
38
39− const ticketInput = useTicketInput({
39+ const [ticketInput, { pass: validTicket }] = useTicketInput({
40 name: 'ticket',
40 name: 'ticket',
41 label: 'Code',
41 label: 'Code',
42 autoFocus: true,
42 autoFocus: true,
43 });
43 });
44− const validTicket = ticketInput.pass;
45
44
46 const goToLogin = useCallback(() => history.popAndPush(ROUTE_NAMES.LOGIN), [
45 const goToLogin = useCallback(() => history.popAndPush(ROUTE_NAMES.LOGIN), [
47 history,
46 history,
src/views/Activate/ActivateDisclaimer.js+3−3
@@ -12,7 +12,7 @@ const TEXT_STYLE = 'f5';
12
12
13export default function ActivateDisclaimer() {
13export default function ActivateDisclaimer() {
14 const { push, names } = useLocalRouter();
14 const { push, names } = useLocalRouter();
15− const checkboxInput = useCheckboxInput({
15+ const [understoodInput, { data: isUnderstood }] = useCheckboxInput({
16 name: 'checkbox',
16 name: 'checkbox',
17 label: 'I acknowledge and understand these rights',
17 label: 'I acknowledge and understand these rights',
18 });
18 });
@@ -70,10 +70,10 @@ export default function ActivateDisclaimer() {
70 <Grid.Item full as={WarningBox}>
70 <Grid.Item full as={WarningBox}>
71 Warning: Nobody but you can restore or reset your Master Ticket
71 Warning: Nobody but you can restore or reset your Master Ticket
72 </Grid.Item>
72 </Grid.Item>
73− <Grid.Item as={CheckboxInput} {...checkboxInput} full />
73+ <Grid.Item as={CheckboxInput} {...understoodInput} full />
74 <Grid.Item
74 <Grid.Item
75 as={ForwardButton}
75 as={ForwardButton}
76− disabled={!checkboxInput.data}
76+ disabled={!isUnderstood}
77 onClick={goToPassport}
77 onClick={goToPassport}
78 solid
78 solid
79 full>
79 full>
src/views/Activate/PassportVerify.js+1−2
@@ -24,14 +24,13 @@ export default function PassportVerify({ className }) {
24 () => [validateExactly(ticket, 'Does not match expected master ticket.')],
24 () => [validateExactly(ticket, 'Does not match expected master ticket.')],
25 [ticket]
25 [ticket]
26 );
26 );
27− const ticketInput = useTicketInput({
27+ const [ticketInput, { pass }] = useTicketInput({
28 name: 'ticket',
28 name: 'ticket',
29 label: 'Master Ticket',
29 label: 'Master Ticket',
30 initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
30 initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
31 autoFocus: true,
31 autoFocus: true,
32 validators,
32 validators,
33 });
33 });
34− const { pass } = ticketInput;
35
34
36 return (
35 return (
37 <Grid className={cn(className, 'auto-rows-min')}>
36 <Grid className={cn(className, 'auto-rows-min')}>
src/views/Login/Keystore.js+1−2
@@ -23,12 +23,11 @@ export default function Keystore({ className }) {
23 // inputs
23 // inputs
24 // keystore: Maybe<String>
24 // keystore: Maybe<String>
25 const [keystore, setKeystore] = useState(Maybe.Nothing());
25 const [keystore, setKeystore] = useState(Maybe.Nothing());
26− const passphraseInput = usePassphraseInput({
26+ const [passphraseInput, { data: passphrase }] = usePassphraseInput({
27 name: 'password',
27 name: 'password',
28 label: 'Keystore password',
28 label: 'Keystore password',
29 autoFocus: true,
29 autoFocus: true,
30 });
30 });
31− const passphrase = passphraseInput.data;
32
31
33 const constructWallet = () => {
32 const constructWallet = () => {
34 try {
33 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';
2import Maybe from 'folktale/maybe';
2import 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';
4import { times } from 'lodash';
4import { times } from 'lodash';
5import * as bip32 from 'bip32';
5import * as bip32 from 'bip32';
6import Transport from '@ledgerhq/hw-transport-u2f';
6import Transport from '@ledgerhq/hw-transport-u2f';
@@ -9,17 +9,22 @@ import * as secp256k1 from 'secp256k1';
9
9
10import { ForwardButton } from 'components/Buttons';
10import { ForwardButton } from 'components/Buttons';
11import { InnerLabelDropdown } from 'components/old/Base';
11import { InnerLabelDropdown } from 'components/old/Base';
12+import { useCheckboxInput, useHdPathInput } from 'components/Inputs';
12
13
13import { useWallet } from 'store/wallet';
14import { useWallet } from 'store/wallet';
14
15
16+import {
17+ LEDGER_LIVE_PATH,
15−import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
18+ LEDGER_LEGACY_PATH,
19+ chopHdPrefix,
20+ addHdPrefix,
21+} from 'lib/ledger';
16import { WALLET_TYPES } from 'lib/wallet';
22import { WALLET_TYPES } from 'lib/wallet';
17import useLoginView from 'lib/useLoginView';
23import useLoginView from 'lib/useLoginView';
18
24
19const pathOptions = [
25const pathOptions = [
20 { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
26 { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
21 { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
27 { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
22− { title: 'Custom path', value: 'custom' },
23];
28];
24
29
25const accountOptions = times(20, i => ({
30const accountOptions = times(20, i => ({
@@ -27,58 +32,70 @@ const accountOptions = times(20, i => ({
27 value: i,
32 value: i,
28}));
33}));
29
34
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−
33export default function Ledger({ className }) {
35export default function Ledger({ className }) {
34 useLoginView(WALLET_TYPES.LEDGER);
36 useLoginView(WALLET_TYPES.LEDGER);
35
37
36 const { setWallet, setWalletHdPath } = useWallet();
38 const { setWallet, setWalletHdPath } = useWallet();
37
39
40+ // ledger has two derivation path _patterns_
38− const [basePath, setBasePath] = useState(LEDGER_LIVE_PATH);
41+ const [basePathPattern, setBasePathPattern] = useState(LEDGER_LIVE_PATH);
39− const [account, setAccount] = useState(0);
42+ // and then we want to be able to select an index within that path
40− const [hdPath, setHdPath] = useState(LEDGER_LIVE_PATH.replace(/x/g, 0));
43+ const [accountIndex, setAccountIndex] = useState(0);
41−
44+
42− const updateHdPath = (basePath, account) => {
43− if (basePath !== 'custom') {
45+ // custom toggle
44− setHdPath(basePath.replace(/x/g, account));
46+ const [customPathInput, { data: useCustomPath }] = useCheckboxInput({
45− }
47+ name: 'customPath',
48+ label: 'Custom HD Path',
49+ autoComplete: 'off',
50+ initialValue: false,
46− };
51+ });
47−
52+
48− const handlePathSelection = basePath => {
53+ // hd path input
49− setBasePath(basePath);
54+ const [
50− updateHdPath(basePath, account);
55+ hdPathInput,
51− };
56+ { data: hdPath },
52−
57+ { setValue: setHdPath },
53− const handleAccountSelection = account => {
58+ ] = useHdPathInput({
54− setAccount(account);
59+ name: 'hdpath',
55− updateHdPath(basePath, account);
60+ label: 'HD Path',
61+ initialValue: basePathPattern.replace(/x/g, 0),
56− };
62+ });
57−
63+
58− const pollDevice = async () => {
64+ const pollDevice = useCallback(async () => {
59 const transport = await Transport.create();
65 const transport = await Transport.create();
60 const eth = new Eth(transport);
66 const eth = new Eth(transport);
61 const path = chopHdPrefix(hdPath);
67 const path = chopHdPrefix(hdPath);
62
68
69+ try {
63− eth.getAddress(path, false, true).then(
70+ const info = await eth.getAddress(path, false, true);
64− info => {
65− const publicKey = Buffer.from(info.publicKey, 'hex');
71+ const publicKey = Buffer.from(info.publicKey, 'hex');
66− const chainCode = Buffer.from(info.chainCode, 'hex');
72+ const chainCode = Buffer.from(info.chainCode, 'hex');
67− const pub = secp256k1.publicKeyConvert(publicKey, true);
73+ const pub = secp256k1.publicKeyConvert(publicKey, true);
68− const hd = bip32.fromPublicKey(pub, chainCode);
74+ const hd = bip32.fromPublicKey(pub, chainCode);
69− setWallet(Maybe.Just(hd));
75+ setWallet(Maybe.Just(hd));
70− setWalletHdPath(addHdPrefix(hdPath));
76+ setWalletHdPath(addHdPrefix(hdPath));
71− },
77+ } catch (error) {
72− _ => {
78+ console.error(error);
73− setWallet(Maybe.Nothing());
79+ setWallet(Maybe.Nothing());
74− }
80+ }
75− );
81+ }, [hdPath, setWallet, setWalletHdPath]);
76− };
82+
77−
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+
78− const basePathTitle = pathOptions.find(o => o.value === basePath).title;
93+ const basePathPatternTitle = pathOptions.find(
94+ o => o.value === basePathPattern
95+ ).title;
79− const accountTitle = accountOptions.find(o => o.value === account).title;
96+ const accountTitle = accountOptions.find(o => o.value === accountIndex).title;
80−
97+
81− const isHTTPS = document.location.protocol === 'https:';
98+ const isHTTPS = document.location.protocol === 'http:';
82
99
83 // when not on https, tell user how to get there
100 // when not on https, tell user how to get there
84 const renderHTTP = () => (
101 const renderHTTP = () => (
@@ -114,67 +131,65 @@ export default function Ledger({ className }) {
114 <li className="mt3">
131 <li className="mt3">
115 Run <code>python bridge-https.py</code>
132 Run <code>python bridge-https.py</code>
116 </li>
133 </li>
134+ <li className="mt3">Return to this page.</li>
117 </Grid.Item>
135 </Grid.Item>
118 </>
136 </>
119 );
137 );
120
138
121 const renderHTTPS = () => (
139 const renderHTTPS = () => (
122 <>
140 <>
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">
124 Connect and authenticate to your Ledger, and then open the "Ethereum"
145 Connect and authenticate to your Ledger, and then open the "Ethereum"
125 application. If you're running on older firmware, make sure the "browser
146 application. If you're running on older firmware, make sure the "browser
126 support" option is turned on. To sign transactions, you'll also need to
147 support" option is turned on. To sign transactions, you'll also need to
127 enable the "contract data" option.
148 enable the "contract data" option.
128 </Grid.Item>
149 </Grid.Item>
129
150
151+ {useCustomPath && (
130− <Grid.Item full as={P}>
152+ <Grid.Item full as={Input} className="mv3" {...hdPathInput} />
131− If you'd like to use a custom derivation path, you may enter it below.
153+ )}
132− </Grid.Item>
133
154
155+ {!useCustomPath && (
156+ <>
134− <Grid.Item
157+ <Grid.Item
135− full
158+ full
136− as={InnerLabelDropdown}
159+ as={InnerLabelDropdown}
137− className="mv4"
160+ className="mv4"
138− title="Derivation path"
161+ title="Derivation path"
139− options={pathOptions}
162+ options={pathOptions}
140− handleUpdate={handlePathSelection}
163+ handleUpdate={setBasePathPattern}
141− currentSelectionTitle={basePathTitle}
164+ currentSelectionTitle={basePathPatternTitle}
142− />
165+ />
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
166+ <Grid.Item
157− full
167+ full
158− as={InnerLabelDropdown}
168+ as={InnerLabelDropdown}
159− className="mt4"
169+ className="mt4"
160− title="Account"
170+ title="Account"
161− options={accountOptions}
171+ options={accountOptions}
162− handleUpdate={handleAccountSelection}
172+ handleUpdate={setAccountIndex}
163− currentSelectionTitle={accountTitle}
173+ currentSelectionTitle={accountTitle}
164− />
174+ />
175+ </>
165 )}
176 )}
166
177
178+ <Grid.Item full as={CheckboxInput} {...customPathInput} />
179+
180+ <Grid.Item
181+ full
182+ as={ForwardButton}
183+ solid
184+ className="mt3"
167− <ForwardButton className="mt3" onClick={pollDevice}>
185+ onClick={pollDevice}>
168 Authenticate
186 Authenticate
169− </ForwardButton>
187+ </Grid.Item>
170 </>
188 </>
171 );
189 );
172
190
173 return (
191 return (
174 <Grid className={className}>
192 <Grid className={className}>
175− <Grid.Item full as={H4}>
176− Authenticate With Your Ledger
177− </Grid.Item>
178 {isHTTPS && renderHTTPS()}
193 {isHTTPS && renderHTTPS()}
179 {!isHTTPS && renderHTTP()}
194 {!isHTTPS && renderHTTP()}
180 </Grid>
195 </Grid>
src/views/Login/Mnemonic.js+25−27
@@ -1,5 +1,5 @@
1−import React, { useCallback, useEffect } from 'react';
1+import React, { useEffect } from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
2+import Maybe from 'folktale/maybe';
3import { Grid, Input, CheckboxInput } from 'indigo-react';
3import { Grid, Input, CheckboxInput } from 'indigo-react';
4
4
5import {
5import {
@@ -19,61 +19,59 @@ export default function Mnemonic({ className }) {
19
19
20 const {
20 const {
21 setWallet,
21 setWallet,
22− authMnemonic,
23 setAuthMnemonic,
22 setAuthMnemonic,
24 walletHdPath,
23 walletHdPath,
25 setWalletHdPath,
24 setWalletHdPath,
26 } = useWallet();
25 } = useWallet();
27
26
28− const advancedInput = useCheckboxInput({
27+ const [advancedInput, { data: useAdvanced }] = useCheckboxInput({
29 name: 'advanced',
28 name: 'advanced',
30 label: 'Passphrase & HD Path',
29 label: 'Passphrase & HD Path',
31 initialValue: false,
30 initialValue: false,
32 });
31 });
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− );
40
32
41− const mnemonicInput = useMnemonicInput({
33+ const [mnemonicInput, { pass, data: mnemonic }] = useMnemonicInput({
42 name: 'mnemonic',
34 name: 'mnemonic',
43 label: 'BIP39 Mnemonic',
35 label: 'BIP39 Mnemonic',
44− initialValue: mnemonic,
45− onValue: _setAuthMnemonic,
46 autoFocus: true,
36 autoFocus: true,
47 });
37 });
48
38
49− const passphraseInput = usePassphraseInput({
39+ const [passphraseInput, { data: passphrase }] = usePassphraseInput({
50 name: 'passphrase',
40 name: 'passphrase',
51 label: '(Optional) Wallet Passphrase',
41 label: '(Optional) Wallet Passphrase',
52 });
42 });
53− const passphrase = passphraseInput.data;
54
43
55− const hdPathInput = useHdPathInput({
44+ const [hdPathInput, { data: hdPath }] = useHdPathInput({
56 name: 'hdpath',
45 name: 'hdpath',
57 label: 'HD Path',
46 label: 'HD Path',
58 initialValue: walletHdPath,
47 initialValue: walletHdPath,
59− onValue: setWalletHdPath,
60 });
48 });
61
49
62− // when the properties change, re-derive wallet
50+ // when the properties change, re-derive wallet and set global state
63 useEffect(() => {
51 useEffect(() => {
64− let mounted = true;
52+ if (pass) {
65− (async () => {
53+ setWalletHdPath(hdPath);
54+ setAuthMnemonic(Maybe.Just(mnemonic));
66− const wallet = walletFromMnemonic(mnemonic, walletHdPath, passphrase);
55+ setWallet(walletFromMnemonic(mnemonic, hdPath, passphrase));
67− mounted && setWallet(wallet);
56+ } else {
68− })();
57+ setAuthMnemonic(Maybe.Nothing());
69− return () => (mounted = false);
58+ setWallet(Maybe.Nothing());
59+ }
60+ }, [
61+ pass,
62+ mnemonic,
70− }, [mnemonic, passphrase, walletHdPath, setWallet]);
63+ passphrase,
64+ hdPath,
65+ setWallet,
66+ setAuthMnemonic,
67+ setWalletHdPath,
68+ ]);
71
69
72 return (
70 return (
73 <Grid className={className}>
71 <Grid className={className}>
74 <Grid.Item full as={Input} {...mnemonicInput} />
72 <Grid.Item full as={Input} {...mnemonicInput} />
75
73
76− {advancedInput.data && (
74+ {useAdvanced && (
77 <>
75 <>
78 <Grid.Item full as={Input} {...passphraseInput} />
76 <Grid.Item full as={Input} {...passphraseInput} />
79 <Grid.Item full as={Input} {...hdPathInput} />
77 <Grid.Item full as={Input} {...hdPathInput} />
src/views/Login/PrivateKey.js+1−2
@@ -14,13 +14,12 @@ export default function PrivateKey({ className }) {
14
14
15 const { setWallet } = useWallet();
15 const { setWallet } = useWallet();
16
16
17− const privateKeyInput = useHexInput({
17+ const [privateKeyInput, { pass, data: privateKey }] = useHexInput({
18 length: 64,
18 length: 64,
19 name: 'privateKey',
19 name: 'privateKey',
20 label: 'Private key',
20 label: 'Private key',
21 autoFocus: true,
21 autoFocus: true,
22 });
22 });
23− const { pass, data: privateKey } = privateKeyInput;
24
23
25 useEffect(() => {
24 useEffect(() => {
26 if (pass) {
25 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';
23
23
24export default function Ticket({ className }) {
24export default function Ticket({ className }) {
25 useLoginView(WALLET_TYPES.TICKET);
25 useLoginView(WALLET_TYPES.TICKET);
26− // globals
26+
27 const { contracts } = useNetwork();
27 const { contracts } = useNetwork();
28 const { setUrbitWallet } = useWallet();
28 const { setUrbitWallet } = useWallet();
29 const { setPointCursor } = usePointCursor();
29 const { setPointCursor } = usePointCursor();
30 const impliedPoint = useImpliedPoint();
30 const impliedPoint = useImpliedPoint();
31
31
32− // inputs
33−
34 // point
32 // point
35− const pointInput = usePointInput({
33+ const [pointInput, { data: pointName }] = usePointInput({
36 name: 'point',
34 name: 'point',
37 initialValue: impliedPoint,
35 initialValue: impliedPoint,
38 autoFocus: true,
36 autoFocus: true,
39 });
37 });
40− const pointName = pointInput.data;
41
38
42 // passphrase
39 // passphrase
43− const passphraseInput = usePassphraseInput({
40+ const [passphraseInput, { data: passphrase }] = usePassphraseInput({
44 name: 'passphrase',
41 name: 'passphrase',
45− label: '(Optional) Wallet Passphrase',
42+ label: 'Wallet Passphrase',
46 });
43 });
47− const passphrase = passphraseInput.data;
48
44
49− const hasPassphraseInput = useCheckboxInput({
45+ const [hasPassphraseInput] = useCheckboxInput({
50 name: 'has-passphrase',
46 name: 'has-passphrase',
51 label: 'Passphrase',
47 label: 'Passphrase',
52 initialValue: false,
48 initialValue: false,
@@ -55,40 +51,36 @@ export default function Ticket({ className }) {
55 // ticket
51 // ticket
56 const [error, setError] = useState();
52 const [error, setError] = useState();
57 const [deriving, setDeriving] = useState(false);
53 const [deriving, setDeriving] = useState(false);
58− const ticketInput = useTicketInput({
54+ const [ticketInput, { data: ticket, pass: validTicket }] = useTicketInput({
59 name: 'ticket',
55 name: 'ticket',
60 label: 'Master Ticket',
56 label: 'Master Ticket',
61 error,
57 error,
62 deriving,
58 deriving,
63 });
59 });
64− const ticket = ticketInput.data;
65
60
66 // shards
61 // shards
67− const shardsInput = useCheckboxInput({
62+ const [shardsInput, { data: isUsingShards }] = useCheckboxInput({
68 name: 'shards',
63 name: 'shards',
69 label: 'Shards',
64 label: 'Shards',
70 initialValue: false,
65 initialValue: false,
71 });
66 });
72
67
73− const shard1Input = useTicketInput({
68+ const [shard1Input, { data: shard1, pass: shard1Pass }] = useTicketInput({
74 name: 'shard1',
69 name: 'shard1',
75 label: 'Shard 1',
70 label: 'Shard 1',
76 });
71 });
77− const shard1 = shard1Input.data;
72+
78− const shard2Input = useTicketInput({
73+ const [shard2Input, { data: shard2, pass: shard2Pass }] = useTicketInput({
79 name: 'shard2',
74 name: 'shard2',
80 label: 'Shard 2',
75 label: 'Shard 2',
81 });
76 });
82− const shard2 = shard2Input.data;
77+
83− const shard3Input = useTicketInput({
78+ const [shard3Input, { data: shard3, pass: shard3Pass }] = useTicketInput({
84 name: 'shard3',
79 name: 'shard3',
85 label: 'Shard 3',
80 label: 'Shard 3',
86 });
81 });
87− const shard3 = shard3Input.data;
88
82
89− const isUsingShards = !!shardsInput.data;
83+ const shardsReady = shard1Pass && shard2Pass && shard3Pass;
90− const shards = [shard1, shard2, shard3];
91− const shardsReady = shards.filter(x => x !== '').length > 1;
92
84
93 // TODO: maybe want to do this only on-go, because wallet derivation is slow...
85 // TODO: maybe want to do this only on-go, because wallet derivation is slow...
94 const deriveWalletFromTicket = useCallback(async () => {
86 const deriveWalletFromTicket = useCallback(async () => {
@@ -144,10 +136,6 @@ export default function Ticket({ className }) {
144 ]);
136 ]);
145
137
146 const deriveWalletFromShards = useCallback(async () => {
138 const deriveWalletFromShards = useCallback(async () => {
147− if (!shardsReady) {
148− return;
149− }
150−
151 const s1 = shard1 || undefined;
139 const s1 = shard1 || undefined;
152 const s2 = shard2 || undefined;
140 const s2 = shard2 || undefined;
153 const s3 = shard3 || undefined;
141 const s3 = shard3 || undefined;
@@ -159,29 +147,22 @@ export default function Ticket({ className }) {
159 } catch (_) {
147 } catch (_) {
160 // do nothing
148 // do nothing
161 }
149 }
162− }, [
163− passphrase,
164− pointName,
165− shardsReady,
166− setUrbitWallet,
150+ }, [passphrase, pointName, setUrbitWallet, shard1, shard2, shard3]);
167− shard1,
168− shard2,
169− shard3,
170− ]);
171
151
172− // use shards if
152+ // derive wallet on change
173 useEffect(() => {
153 useEffect(() => {
174 if (isUsingShards && shardsReady) {
154 if (isUsingShards && shardsReady) {
175 deriveWalletFromShards();
155 deriveWalletFromShards();
176− }
156+ } else if (validTicket) {
177− }, [isUsingShards, shardsReady, deriveWalletFromShards]);
178−
179− // verify ticket if not using shards
180− useEffect(() => {
181− if (!isUsingShards) {
182 deriveWalletFromTicket();
157 deriveWalletFromTicket();
183 }
158 }
159+ }, [
160+ isUsingShards,
161+ validTicket,
162+ shardsReady,
163+ deriveWalletFromShards,
184− }, [isUsingShards, deriveWalletFromTicket]);
164+ deriveWalletFromTicket,
165+ ]);
185
166
186 return (
167 return (
187 <Grid className={className}>
168 <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';
3import Maybe from 'folktale/maybe';
2import Maybe from 'folktale/maybe';
3+import * as bip32 from 'bip32';
4import { times } from 'lodash';
4import { times } from 'lodash';
5import TrezorConnect from 'trezor-connect';
5import TrezorConnect from 'trezor-connect';
6import * as secp256k1 from 'secp256k1';
6import * as secp256k1 from 'secp256k1';
7−import { P, Input, Grid, H5 } from 'indigo-react';
7+import { Text, Input, Grid, H5, CheckboxInput } from 'indigo-react';
8
8
9import { ForwardButton } from 'components/Buttons';
9import { ForwardButton } from 'components/Buttons';
10import { InnerLabelDropdown } from 'components/old/Base';
10import { InnerLabelDropdown } from 'components/old/Base';
@@ -14,26 +14,39 @@ import { WALLET_TYPES } from 'lib/wallet';
14
14
15import { useWallet } from 'store/wallet';
15import { useWallet } from 'store/wallet';
16import useLoginView from 'lib/useLoginView';
16import useLoginView from 'lib/useLoginView';
17+import { useHdPathInput, useCheckboxInput } from 'components/Inputs';
17
18
18−const accountOptions = [
19+const accountOptions = times(20, i => ({
19− { title: 'Custom path', value: 'custom' },
20− ...times(20, i => ({ title: `Account #${i + 1}`, value: i })),
20+ title: `Account #${i + 1}`,
21−];
21+ value: i,
22+}));
22
23
24+// see Ledger.js for context — Trezor is basicaly Ledger with less complexity
23export default function Trezor({ className }) {
25export default function Trezor({ className }) {
24 useLoginView(WALLET_TYPES.TREZOR);
26 useLoginView(WALLET_TYPES.TREZOR);
25
27
26 const { setWallet, setWalletHdPath } = useWallet();
28 const { setWallet, setWalletHdPath } = useWallet();
27
29
28− const [hdPath, setHdPath] = useState(TREZOR_PATH.replace(/x/g, 0));
30+ const [accountIndex, setAccountIndex] = useState(0);
29− const [account, setAccount] = useState(0);
31+
30−
32+ // custom toggle
31− const handleAccountSelection = account => {
33+ const [customPathInput, { data: useCustomPath }] = useCheckboxInput({
32− setAccount(account);
34+ name: 'customPath',
33− if (account !== 'custom') {
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',
34− setHdPath(TREZOR_PATH.replace(/x/g, account));
48+ initialValue: TREZOR_PATH.replace(/x/g, 0),
35− }
36− };
49+ });
37
50
38 const pollDevice = async () => {
51 const pollDevice = async () => {
39 TrezorConnect.manifest({
52 TrezorConnect.manifest({
@@ -41,22 +54,32 @@ export default function Trezor({ className }) {
41 appUrl: 'https://github.com/urbit/bridge',
54 appUrl: 'https://github.com/urbit/bridge',
42 });
55 });
43
56
44− TrezorConnect.getPublicKey({ path: hdPath }).then(info => {
57+ const info = await TrezorConnect.getPublicKey({
45− if (info.success === true) {
58+ path: hdPath,
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− }
56 });
59 });
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+ }
57 };
72 };
58
73
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+
59− const accountTitle = accountOptions.find(o => o.value === account).title;
82+ const accountTitle = accountOptions.find(o => o.value === accountIndex).title;
60
83
61 return (
84 return (
62 <Grid className={className}>
85 <Grid className={className}>
@@ -64,34 +87,29 @@ export default function Trezor({ className }) {
64 Authenticate With Your Trezor
87 Authenticate With Your Trezor
65 </Grid.Item>
88 </Grid.Item>
66
89
67− <Grid.Item full as={P}>
90+ <Grid.Item full as={Text} className="f6">
68 Connect and authenticate to your Trezor. If you'd like to use a custom
91 Connect and authenticate to your Trezor. If you'd like to use a custom
69 derivation path, you may enter it below.
92 derivation path, you may enter it below.
70 </Grid.Item>
93 </Grid.Item>
71
94
72− <Grid.Item
95+ {useCustomPath && (
73− full
96+ <Grid.Item full as={Input} className="mv3" {...hdPathInput} />
74− as={InnerLabelDropdown}
97+ )}
75− className="mv4"
98+
76− title="Account"
99+ {!useCustomPath && (
77− options={accountOptions}
78− handleUpdate={handleAccountSelection}
79− currentSelectionTitle={accountTitle}
80− />
81−
82− {account === 'custom' && (
83 <Grid.Item
100 <Grid.Item
84 full
101 full
85− as={Input}
102+ as={InnerLabelDropdown}
86 className="mv4"
103 className="mv4"
87− name="hdPath"
104+ title="Account"
88− label="HD path"
105+ options={accountOptions}
89− autoComplete="off"
106+ handleUpdate={setAccountIndex}
90− initialValue={hdPath}
107+ currentSelectionTitle={accountTitle}
91− onValue={setHdPath}
92 />
108 />
93 )}
109 )}
94
110
111+ <Grid.Item full as={CheckboxInput} {...customPathInput} />
112+
95 <Grid.Item
113 <Grid.Item
96 full
114 full
97 as={ForwardButton}
115 as={ForwardButton}
src/views/ViewPoint.js+1−2
@@ -17,11 +17,10 @@ import { usePointInput } from 'components/Inputs';
17export default function ViewPoint() {
17export default function ViewPoint() {
18 const history = useHistory();
18 const history = useHistory();
19 const { setPointCursor } = usePointCursor();
19 const { setPointCursor } = usePointCursor();
20− const pointInput = usePointInput({
20+ const [pointInput, { data: pointName, pass }] = usePointInput({
21 name: 'point',
21 name: 'point',
22 autoFocus: true,
22 autoFocus: true,
23 });
23 });
24− const { data: pointName, pass } = pointInput;
25
24
26 const disabled = !pass;
25 const disabled = !pass;
27 const goForward = useCallback(() => {
26 const goForward = useCallback(() => {