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