on e366ef3 help sign in
bridge/src/views/Ledger.js
225 lines 6.8 kB history raw
1import * as bip32 from 'bip32';
2import React from 'react';
3import Maybe from 'folktale/maybe';
4import { Button } from '../components/old/Base';
5import { Row, Col, H1, P, H2 } from '../components/old/Base';
6import { Input, InnerLabel, InnerLabelDropdown } from '../components/old/Base';
7import Transport from '@ledgerhq/hw-transport-u2f';
8import Eth from '@ledgerhq/hw-app-eth';
9import * as secp256k1 from 'secp256k1';
11import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from '../lib/ledger';
12import { ROUTE_NAMES } from '../lib/routeNames';
13import { withHistory } from '../store/history';
14import { compose } from '../lib/lib';
15import { withWallet } from '../store/wallet';
17const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
19const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
21class Ledger extends React.Component {
22 constructor(props) {
23 super(props);
25 this.state = {
26 basePath: LEDGER_LIVE_PATH,
27 hdpath: LEDGER_LIVE_PATH.replace(/x/g, 0),
28 account: 0,
29 };
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 }
38 handlePathSelection(basePath) {
39 this.updateHdPath(basePath, this.state.account);
40 }
42 handleAccountSelection(account) {
43 this.updateHdPath(this.state.basePath, account);
44 }
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 }
54 handleHdPathInput(hdpath) {
55 this.setState({ hdpath });
56 }
58 async pollDevice() {
59 const { setWallet, setWalletHdPath } = this.props;
60 const { hdpath } = this.state;
62 const transport = await Transport.create();
63 const eth = new Eth(transport);
64 const path = chopHdPrefix(hdpath);
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 }
81 render() {
82 const { history, wallet } = this.props;
83 const { basePath, hdpath, account } = this.state;
84 const { handlePathSelection, handleAccountSelection } = this;
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;
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;
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 );
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 );
141 return (
142 <Row>
143 <Col className={'measure-md'}>
144 <H1>{'Authenticate With Your Ledger'}</H1>
146 <H2>{'Running on HTTPS?'}</H2>
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>
156 <P>
157 {`If you'd like to use a custom derivation path, you may enter
158 it below.`}
159 </P>
161 <H2>{'Running on HTTP?'}</H2>
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>
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>
199 {basePathSelection}
200 {truePathSelection}
202 <Button
203 prop-size={'wide lg'}
204 className={'mt-8'}
205 onClick={this.pollDevice}>
206 {'Authenticate →'}
207 </Button>
209 <Button
210 className={'mt-8'}
211 prop-size={'wide lg'}
212 disabled={Maybe.Nothing.hasInstance(wallet)}
213 onClick={() => history.popAndPush(ROUTE_NAMES.SHIPS)}>
214 {'Continue →'}
215 </Button>
216 </Col>
217 </Row>
218 );
219 }
222export default compose(
223 withHistory,
224 withWallet
225)(Ledger);