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