@@ -0,0 +1,224 @@
1+import * as bip32 from 'bip32';
2+import React from 'react';
3+import 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';
11+import Transport from '@ledgerhq/hw-transport-u2f';
12+import Eth from '@ledgerhq/hw-app-eth';
13+import * as secp256k1 from 'secp256k1';
14+
15+import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from '../../lib/ledger';
16+import { compose } from '../../lib/lib';
17+import { withWallet } from '../../store/wallet';
18+
19+const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
20+
21+const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
22+
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;
50+ if (basePath !== 'custom') {
51+ hdpath = basePath.replace(/x/g, account);
52+ }
53+ this.setState({ basePath, account, hdpath });
54+ }
55+
56+ handleHdPathInput(hdpath) {
57+ this.setState({ hdpath });
58+ }
59+
60+ async pollDevice() {
61+ const { setWallet, setWalletHdPath } = this.props;
62+ const { hdpath } = this.state;
63+
64+ const transport = await Transport.create();
65+ const eth = new Eth(transport);
66+ const path = chopHdPrefix(hdpath);
67+
68+ eth.getAddress(path, false, true).then(
69+ info => {
70+ const publicKey = Buffer.from(info.publicKey, 'hex');
71+ const chainCode = Buffer.from(info.chainCode, 'hex');
72+ const pub = secp256k1.publicKeyConvert(publicKey, true);
73+ const hd = bip32.fromPublicKey(pub, chainCode);
74+ setWallet(Maybe.Just(hd));
75+ setWalletHdPath(addHdPrefix(hdpath));
76+ },
77+ _ => {
78+ setWallet(Maybe.Nothing());
79+ }
80+ );
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 = (
108+ <InnerLabelDropdown
109+ className="mt-8"
110+ options={pathOptions}
111+ handleUpdate={handlePathSelection}
112+ title={'Derivation path'}
113+ currentSelectionTitle={basePathTitle}
114+ fullWidth={true}
115+ />
116+ );
117+
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>
220+ );
221+ }
222+}
223+
224+export default compose(withWallet)(Ledger);