@@ -1,69 +1,56 @@
11import * as bip32 from 'bip32';
2−import React from 'react';
2+import React, { useState } from 'react';
3+import { times } from 'lodash';
34import Maybe from 'folktale/maybe';
4−import { Button } from '../../components/old/Base';
5−import { Row, Col, H1, P, H2 } from '../../components/old/Base';
6−import {
7− Input,
8− InnerLabel,
9− InnerLabelDropdown,
10−} from '../../components/old/Base';
115import Transport from '@ledgerhq/hw-transport-u2f';
126import Eth from '@ledgerhq/hw-app-eth';
137import * as secp256k1 from 'secp256k1';
148
15−import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from '../../lib/ledger';
16−import { compose } from '../../lib/lib';
17−import { withWallet } from '../../store/wallet';
9+import View from 'components/View';
10+import { ForwardButton } from 'components/Buttons';
11+import { H1, H2, P, Input } from 'indigo-react';
12+import { InnerLabel, InnerLabelDropdown } from 'components/old/Base';
13+
14+import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
15+import { useWallet } from 'store/wallet';
16+
17+const pathOptions = [
18+ { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
19+ { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
20+ { title: 'Custom path', value: 'custom' },
21+];
1822
1923const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
2024
2125const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
2226
23−class Ledger extends React.Component {
24− constructor(props) {
25− super(props);
26−
27− this.state = {
28− basePath: LEDGER_LIVE_PATH,
29− hdpath: LEDGER_LIVE_PATH.replace(/x/g, 0),
30− account: 0,
31− };
32−
33− this.pollDevice = this.pollDevice.bind(this);
34− this.handleHdPathInput = this.handleHdPathInput.bind(this);
35− this.handlePathSelection = this.handlePathSelection.bind(this);
36− this.handleAccountSelection = this.handleAccountSelection.bind(this);
37− this.updateHdPath = this.updateHdPath.bind(this);
38− }
39−
40− handlePathSelection(basePath) {
41− this.updateHdPath(basePath, this.state.account);
42− }
43−
44− handleAccountSelection(account) {
45− this.updateHdPath(this.state.basePath, account);
46− }
47−
48− updateHdPath(basePath, account) {
49− let hdpath = this.state.hdpath;
27+export default function Ledger({ loginCompleted }) {
28+ const { wallet, setWallet, setWalletHdPath } = useWallet();
29+
30+ const [basePath, setBasePath] = useState(LEDGER_LIVE_PATH);
31+ const [account, setAccount] = useState(0);
32+ const [hdPath, setHdPath] = useState(LEDGER_LIVE_PATH.replace(/x/g, 0));
33+
34+ const updateHdPath = (basePath, account) => {
5035 if (basePath !== 'custom') {
51− hdpath = basePath.replace(/x/g, account);
36+ setHdPath(basePath.replace(/x/g, account));
5237 }
53− this.setState({ basePath, account, hdpath });
54− }
38+ };
5539
56− handleHdPathInput(hdpath) {
57− this.setState({ hdpath });
58− }
40+ const handlePathSelection = basePath => {
41+ setBasePath(basePath);
42+ updateHdPath(basePath, account);
43+ };
5944
60− async pollDevice() {
61− const { setWallet, setWalletHdPath } = this.props;
62− const { hdpath } = this.state;
45+ const handleAccountSelection = account => {
46+ setAccount(account);
47+ updateHdPath(basePath, account);
48+ };
6349
50+ const pollDevice = async () => {
6451 const transport = await Transport.create();
6552 const eth = new Eth(transport);
66− const path = chopHdPrefix(hdpath);
53+ const path = chopHdPrefix(hdPath);
6754
6855 eth.getAddress(path, false, true).then(
6956 info => {
@@ -72,153 +59,122 @@ class Ledger extends React.Component {
7259 const pub = secp256k1.publicKeyConvert(publicKey, true);
7360 const hd = bip32.fromPublicKey(pub, chainCode);
7461 setWallet(Maybe.Just(hd));
75− setWalletHdPath(addHdPrefix(hdpath));
62+ setWalletHdPath(addHdPrefix(hdPath));
7663 },
7764 _ => {
7865 setWallet(Maybe.Nothing());
7966 }
8067 );
81− }
82−
83− render() {
84− const { wallet } = this.props;
85− const { basePath, hdpath, account } = this.state;
86− const { handlePathSelection, handleAccountSelection } = this;
87−
88− const pathOptions = [
89− { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
90− { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
91− {
92− title: 'Custom path',
93− value: 'custom',
94− },
95− ];
96− let basePathTitle = pathOptions.find(o => o.value === basePath).title;
97−
98− let accountOptions = [];
99− for (let i = 0; i < 20; i++) {
100− accountOptions.push({
101− title: 'Account #' + (i + 1),
102− value: i,
103− });
104− }
105− let accountTitle = accountOptions.find(o => o.value === account).title;
106−
107− const basePathSelection = (
68+ };
69+
70+ const basePathTitle = pathOptions.find(o => o.value === basePath).title;
71+
72+ const accountOptions = times(20, i => ({
73+ title: `Account #${i + 1}`,
74+ value: i,
75+ }));
76+ const accountTitle = accountOptions.find(o => o.value === account).title;
77+
78+ const basePathSelection = (
79+ <InnerLabelDropdown
80+ className="mt-8"
81+ options={pathOptions}
82+ handleUpdate={handlePathSelection}
83+ title="Derivation path"
84+ currentSelectionTitle={basePathTitle}
85+ fullWidth={true}
86+ />
87+ );
88+
89+ const truePathSelection =
90+ basePath === 'custom' ? (
91+ <Input
92+ className="mt3"
93+ name="hdPath"
94+ label="HD path"
95+ autocomplete="off"
96+ initialValue={addHdPrefix(hdPath)}
97+ onValue={setHdPath}
98+ />
99+ ) : (
108100 <InnerLabelDropdown
109− className="mt-8"
110− options={pathOptions}
111− handleUpdate={handlePathSelection}
112− title={'Derivation path'}
113− currentSelectionTitle={basePathTitle}
101+ className="mt-4"
102+ prop-size="md"
103+ prop-format="innerLabel"
104+ options={accountOptions}
105+ handleUpdate={handleAccountSelection}
106+ title="Account"
107+ currentSelectionTitle={accountTitle}
114108 fullWidth={true}
115109 />
116110 );
117111
118− const truePathSelection =
119− basePath === 'custom' ? (
120− <Input
121− className="pt-8 mt-4 text-mono"
122− prop-size="md"
123− prop-format="innerLabel"
124− name="hdpath"
125− value={addHdPrefix(hdpath)}
126− autocomplete="off"
127− onChange={this.handleHdPathInput}>
128− <InnerLabel>{'HD Path'}</InnerLabel>
129− </Input>
130− ) : (
131− <InnerLabelDropdown
132− className="mt-4"
133− prop-size="md"
134− prop-format="innerLabel"
135− options={accountOptions}
136− handleUpdate={handleAccountSelection}
137− title={'Account'}
138− currentSelectionTitle={accountTitle}
139− fullWidth={true}
140− />
141− );
142−
143− return (
144− <Row>
145− <Col className={'measure-md'}>
146− <H1>{'Authenticate With Your Ledger'}</H1>
147−
148− <H2>{'Running on HTTPS?'}</H2>
149−
150− <P>
151− {`Connect and authenticate to your Ledger, and then open the
152− "Ethereum" application. If you're running on older firmware,
153− make sure the "browser support" option is turned on. To sign
154− transactions, you'll also need to enable the "contract data"
155− option.`}
156− </P>
157−
158− <P>
159− {`If you'd like to use a custom derivation path, you may enter
160− it below.`}
161− </P>
162−
163− <H2>{'Running on HTTP?'}</H2>
164−
165− <P>{`To authenticate and sign transactions with a Ledger, Bridge
166− must be serving over HTTPS on localhost. You can do this via the
167− following:`}</P>
168−
169− <ol className={'measure-md'}>
170− <li className={'mt-4'}>
171− {'Install'}{' '}
172− <a
173− target={'_blank'}
174− href={'https://github.com/FiloSottile/mkcert'}>
175− {'mkcert'}
176− </a>
177− </li>
178− <li className={'mt-4'}>
179− {'Install a local certificate authority via '}
180− <code>{'mkcert -install'}</code>
181− </li>
182− <li className={'mt-4'}>
183− {'In your '}
184− <code>{'bridge'}</code>
185− {
186− ' directory, generate a certificate valid for localhost via '
187− }{' '}
188− <code>{'mkcert localhost'}</code>
189− {'.'}
190− {'This will produce two files: '}
191− <code>{'localhost.pem'}</code> {', the local certificate, and '}
192− <code>{'localhost-key.pem'}</code>
193− {', its corresponding private key.'}
194− </li>
195− <li className={'mt-4'}>
196− {'Run '}
197− <code>{'python bridge-https.py'}</code>
198− </li>
199− </ol>
200−
201− {basePathSelection}
202− {truePathSelection}
203−
204− <Button
205− prop-size={'wide lg'}
206− className={'mt-8'}
207− onClick={this.pollDevice}>
208− {'Authenticate →'}
209− </Button>
210−
211− <Button
212− className={'mt-8'}
213− prop-size={'wide lg'}
214− disabled={Maybe.Nothing.hasInstance(wallet)}
215− onClick={this.props.loginCompleted}>
216− {'Continue →'}
217− </Button>
218− </Col>
219− </Row>
112+ // when not on https, tell user how to get there
113+ const body =
114+ document.location.protocol !== 'https:' ? (
115+ <>
116+ <H2>Running on HTTP?</H2>
117+
118+ <P>
119+ To authenticate and sign transactions with a Ledger, Bridge must be
120+ serving over HTTPS on localhost. You can do this via the following:
121+ </P>
122+
123+ <ol className="measure-md">
124+ <li className="mt-4">
125+ Install
126+ <a target="_blank" href="https://github.com/FiloSottile/mkcert">
127+ mkcert
128+ </a>
129+ </li>
130+ <li className="mt-4">
131+ Install a local certificate authority via{' '}
132+ <code>mkcert -install</code>
133+ </li>
134+ <li className="mt-4">
135+ In your <code>bridge</code> directory, generate a certificate valid
136+ for localhost via <code>mkcert localhost</code>. This will produce
137+ two files: <code>localhost.pem</code>, the local certificate, and
138+ <code>localhost-key.pem</code> , its corresponding private key.
139+ </li>
140+ <li className="mt-4">
141+ Run <code>python bridge-https.py</code>
142+ </li>
143+ </ol>
144+ </>
145+ ) : (
146+ <>
147+ <P>
148+ Connect and authenticate to your Ledger, and then open the "Ethereum"
149+ application. If you're running on older firmware, make sure the
150+ "browser support" option is turned on. To sign transactions, you'll
151+ also need to enable the "contract data" option.
152+ </P>
153+
154+ <P>
155+ If you'd like to use a custom derivation path, you may enter it below.
156+ </P>
157+
158+ {basePathSelection}
159+ {truePathSelection}
160+
161+ <ForwardButton className={'mt3'} onClick={pollDevice}>
162+ {'Authenticate'}
163+ </ForwardButton>
164+
165+ <ForwardButton
166+ className={'mt3'}
167+ disabled={Maybe.Nothing.hasInstance(wallet)}
168+ onClick={loginCompleted}>
169+ {'Continue'}
170+ </ForwardButton>
171+ </>
220172 );
221− }
222−}
223173
224−export default compose(withWallet)(Ledger);
174+ return (
175+ <View>
176+ <H1>Authenticate With Your Ledger</H1>
177+ {body}
178+ </View>
179+ );
180+}