@@ -1,190 +1,210 @@
1−import * as bip32 from 'bip32';
2−import React, { useState } from 'react';
1+import React, { useCallback, useEffect } from 'react';
2+import cn from 'classnames';
3+import { Just, Nothing } from 'folktale/maybe';
4+import {
5+ P,
6+ Text,
7+ Input,
8+ Grid,
9+ H5,
10+ CheckboxInput,
11+ SelectInput,
12+} from 'indigo-react';
313import { times } from 'lodash';
4−import Maybe from 'folktale/maybe';
14+import * as bip32 from 'bip32';
515import Transport from '@ledgerhq/hw-transport-u2f';
616import Eth from '@ledgerhq/hw-app-eth';
717import * as secp256k1 from 'secp256k1';
8−import { H1, H2, P, Input } from 'indigo-react';
9−
10−import View from 'components/View';
11−import { ForwardButton } from 'components/Buttons';
12−import { InnerLabelDropdown } from 'components/old/Base';
1318
1419import { useWallet } from 'store/wallet';
1520
16−import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
17−import useWalletType from 'lib/useWalletType';
21+import { ForwardButton } from 'components/Buttons';
22+
23+import {
24+ useCheckboxInput,
25+ useHdPathInput,
26+ useSelectInput,
27+} from 'lib/useInputs';
28+
29+import {
30+ LEDGER_LIVE_PATH,
31+ LEDGER_LEGACY_PATH,
32+ chopHdPrefix,
33+ addHdPrefix,
34+} from 'lib/ledger';
1835import { WALLET_TYPES } from 'lib/wallet';
19−import useResetPointCursor from 'lib/useResetPointCursor';
36+import useLoginView from 'lib/useLoginView';
37+import useBreakpoints from 'lib/useBreakpoints';
2038
21−const pathOptions = [
22− { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
23− { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
24− { title: 'Custom path', value: 'custom' },
39+const PATH_OPTIONS = [
40+ { text: 'Ledger Live', value: LEDGER_LIVE_PATH },
41+ { text: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
2542];
2643
27−const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
28−
29−const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
30−
31−export default function Ledger({ loginCompleted }) {
32− useResetPointCursor();
33− useWalletType(WALLET_TYPES.LEDGER);
34−
35− const { wallet, setWallet, setWalletHdPath } = useWallet();
36−
37− const [basePath, setBasePath] = useState(LEDGER_LIVE_PATH);
38− const [account, setAccount] = useState(0);
39− const [hdPath, setHdPath] = useState(LEDGER_LIVE_PATH.replace(/x/g, 0));
40−
41− const updateHdPath = (basePath, account) => {
42− if (basePath !== 'custom') {
43− setHdPath(basePath.replace(/x/g, account));
44− }
45− };
46−
47− const handlePathSelection = basePath => {
48− setBasePath(basePath);
49− updateHdPath(basePath, account);
50− };
51−
52− const handleAccountSelection = account => {
53− setAccount(account);
54− updateHdPath(basePath, account);
55− };
56−
57− const pollDevice = async () => {
44+const ACCOUNT_OPTIONS = times(20, i => ({
45+ text: `Account #${i + 1}`,
46+ value: i,
47+}));
48+
49+export default function Ledger({ className }) {
50+ useLoginView(WALLET_TYPES.LEDGER);
51+
52+ const { setWallet, setWalletHdPath } = useWallet();
53+
54+ // derivation path input
55+ const [derivationPathInput, { data: basePathPattern }] = useSelectInput({
56+ name: 'derivationpath',
57+ label: 'Derivation Path',
58+ placeholder: 'Choose path pattern...',
59+ options: PATH_OPTIONS,
60+ });
61+
62+ // account input
63+ const [accountInput, { data: accountIndex }] = useSelectInput({
64+ name: 'account',
65+ label: 'Account',
66+ placeholder: 'Choose account...',
67+ options: ACCOUNT_OPTIONS,
68+ });
69+
70+ // custom toggle
71+ const [customPathInput, { data: useCustomPath }] = useCheckboxInput({
72+ name: 'customPath',
73+ label: 'Custom HD Path',
74+ autoComplete: 'off',
75+ initialValue: false,
76+ });
77+
78+ // hd path input
79+ const [
80+ hdPathInput,
81+ { data: hdPath },
82+ { setValue: setHdPath },
83+ ] = useHdPathInput({
84+ name: 'hdpath',
85+ label: 'HD Path',
86+ initialValue: basePathPattern.replace(/x/g, 0),
87+ });
88+
89+ const pollDevice = useCallback(async () => {
5890 const transport = await Transport.create();
5991 const eth = new Eth(transport);
6092 const path = chopHdPrefix(hdPath);
6193
62− eth.getAddress(path, false, true).then(
63− info => {
64− const publicKey = Buffer.from(info.publicKey, 'hex');
65− const chainCode = Buffer.from(info.chainCode, 'hex');
66− const pub = secp256k1.publicKeyConvert(publicKey, true);
67− const hd = bip32.fromPublicKey(pub, chainCode);
68− setWallet(Maybe.Just(hd));
69− setWalletHdPath(addHdPrefix(hdPath));
70− },
71− _ => {
72− setWallet(Maybe.Nothing());
73− }
74− );
75− };
76−
77− const basePathTitle = pathOptions.find(o => o.value === basePath).title;
78−
79− const accountOptions = times(20, i => ({
80− title: `Account #${i + 1}`,
81− value: i,
82− }));
83− const accountTitle = accountOptions.find(o => o.value === account).title;
84−
85− const basePathSelection = (
86− <InnerLabelDropdown
87− className="mt-8"
88− options={pathOptions}
89− handleUpdate={handlePathSelection}
90− title="Derivation path"
91− currentSelectionTitle={basePathTitle}
92− fullWidth={true}
93− />
94− );
94+ try {
95+ const info = await eth.getAddress(path, false, true);
96+ const publicKey = Buffer.from(info.publicKey, 'hex');
97+ const chainCode = Buffer.from(info.chainCode, 'hex');
98+ const pub = secp256k1.publicKeyConvert(publicKey, true);
99+ const hd = bip32.fromPublicKey(pub, chainCode);
100+ setWallet(Just(hd));
101+ setWalletHdPath(addHdPrefix(hdPath));
102+ } catch (error) {
103+ console.error(error);
104+ setWallet(Nothing());
105+ }
106+ }, [hdPath, setWallet, setWalletHdPath]);
107+
108+ // when the base path pattern or the account index changes
109+ // update the hd path in our input
110+ useEffect(() => {
111+ if (useCustomPath) {
112+ // updated by useForm
113+ } else {
114+ setHdPath(basePathPattern.replace(/x/g, accountIndex));
115+ }
116+ }, [useCustomPath, setHdPath, basePathPattern, accountIndex]);
95117
96− const truePathSelection =
97− basePath === 'custom' ? (
98− <Input
99− className="mt3"
100− name="hdPath"
101− label="HD path"
102− autocomplete="off"
103− initialValue={addHdPrefix(hdPath)}
104− onValue={setHdPath}
105− />
106− ) : (
107− <InnerLabelDropdown
108− className="mt-4"
109− prop-size="md"
110− prop-format="innerLabel"
111− options={accountOptions}
112− handleUpdate={handleAccountSelection}
113− title="Account"
114− currentSelectionTitle={accountTitle}
115− fullWidth={true}
116− />
117− );
118+ const full = useBreakpoints([true, true, false]);
119+ const half = useBreakpoints([false, false, true]);
120+ const isHTTPS = document.location.protocol === 'https:';
118121
119122 // when not on https, tell user how to get there
120− const body =
121− document.location.protocol !== 'https:' ? (
122− <>
123− <H2>Running on HTTP?</H2>
124−
125− <P>
126− To authenticate and sign transactions with a Ledger, Bridge must be
127− serving over HTTPS on localhost. You can do this via the following:
128− </P>
129−
130− <ol className="measure-md">
131− <li className="mt-4">
132− Install
133− <a
134− target="_blank"
135− href="https://github.com/FiloSottile/mkcert"
136− rel="noopener noreferrer">
137− mkcert
138− </a>
139− </li>
140− <li className="mt-4">
141− Install a local certificate authority via{' '}
142− <code>mkcert -install</code>
143− </li>
144− <li className="mt-4">
145− In your <code>bridge</code> directory, generate a certificate valid
146− for localhost via <code>mkcert localhost</code>. This will produce
147− two files: <code>localhost.pem</code>, the local certificate, and
148− <code>localhost-key.pem</code> , its corresponding private key.
149− </li>
150− <li className="mt-4">
151− Run <code>python bridge-https.py</code>
152− </li>
153− </ol>
154− </>
155− ) : (
156− <>
157− <P>
158− Connect and authenticate to your Ledger, and then open the "Ethereum"
159− application. If you're running on older firmware, make sure the
160− "browser support" option is turned on. To sign transactions, you'll
161− also need to enable the "contract data" option.
162− </P>
163−
164− <P>
165− If you'd like to use a custom derivation path, you may enter it below.
166− </P>
167−
168− {basePathSelection}
169− {truePathSelection}
170−
171− <ForwardButton className={'mt3'} onClick={pollDevice}>
172− {'Authenticate'}
173− </ForwardButton>
174−
175− <ForwardButton
176− className={'mt3'}
177− disabled={Maybe.Nothing.hasInstance(wallet)}
178− onClick={loginCompleted}>
179− {'Continue'}
180− </ForwardButton>
181− </>
182− );
123+ const renderHTTP = () => (
124+ <>
125+ <Grid.Item full as={H5}>
126+ Running on HTTP?
127+ </Grid.Item>
128+
129+ <Grid.Item full as={P}>
130+ To authenticate and sign transactions with a Ledger, Bridge must be
131+ serving over HTTPS on localhost. You can do this via the following:
132+ </Grid.Item>
133+
134+ <Grid.Item full as="ol">
135+ <li>
136+ Install{' '}
137+ <a
138+ target="_blank"
139+ href="https://github.com/FiloSottile/mkcert"
140+ rel="noopener noreferrer">
141+ mkcert
142+ </a>
143+ </li>
144+ <li className="mt3">
145+ Install a local certificate authority via <code>mkcert -install</code>
146+ </li>
147+ <li className="mt3">
148+ In your <code>bridge</code> directory, generate a certificate valid
149+ for localhost via <code>mkcert localhost</code>. This will produce two
150+ files: <code>localhost.pem</code>, the local certificate, and
151+ <code>localhost-key.pem</code> , its corresponding private key.
152+ </li>
153+ <li className="mt3">
154+ Run <code>python bridge-https.py</code>
155+ </li>
156+ <li className="mt3">Return to this page.</li>
157+ </Grid.Item>
158+ </>
159+ );
160+
161+ const renderHTTPS = () => (
162+ <>
163+ <Grid.Item full as={H5}>
164+ Authenticate With Your Ledger
165+ </Grid.Item>
166+ <Grid.Item full as={Text} className="f6 mb3">
167+ Connect and authenticate to your Ledger, and then open the "Ethereum"
168+ application. If you're running on older firmware, make sure the "browser
169+ support" option is turned on. To sign transactions, you'll also need to
170+ enable the "contract data" option.
171+ </Grid.Item>
172+
173+ {useCustomPath && <Grid.Item full as={Input} {...hdPathInput} />}
174+
175+ {!useCustomPath && (
176+ <>
177+ <Grid.Item
178+ full={full}
179+ half={half && 1}
180+ as={SelectInput}
181+ className={cn({ pr1: half })}
182+ {...derivationPathInput}
183+ />
184+ <Grid.Item
185+ full={full}
186+ half={half && 2}
187+ as={SelectInput}
188+ className={cn({ pl1: half })}
189+ {...accountInput}
190+ />
191+ </>
192+ )}
193+
194+ <Grid.Item full as={CheckboxInput} className="mv3" {...customPathInput} />
195+
196+ <Grid.Item
197+ full
198+ as={ForwardButton}
199+ solid
200+ className="mt2"
201+ onClick={pollDevice}>
202+ Authenticate
203+ </Grid.Item>
204+ </>
205+ );
183206
184207 return (
185− <View>
186− <H1>Authenticate With Your Ledger</H1>
187− {body}
188− </View>
208+ <Grid className={className}>{isHTTPS ? renderHTTPS() : renderHTTP()}</Grid>
189209 );
190210}