on master help sign in
Add remaining advanced login methods
commit4862533e5844b7e3009192df858b3de94cd5c537 authorFang <git@fang.io> dateThu 20 Jun 2019, 15:57 (7 years ago) parenta14d3fb
6 files changed+861−14
src/bridge/views/Login/Advanced.js+22−14 src/bridge/views/Login/Keystore.js+167 src/bridge/views/Login/Ledger.js+225 src/bridge/views/Login/PrivateKey.js+81 src/bridge/views/Login/Shards.js+219 src/bridge/views/Login/Trezor.js+147
src/bridge/views/Login/Advanced.js+22−14
@@ -5,8 +5,12 @@ import { InnerLabelDropdown } from '../../components/Base';
55import { Row, Col } from '../../components/Base';
66
77import Ticket from './Ticket';
8+import Shards from './Shards';
9+import Ledger from './Ledger';
10+import Trezor from './Trezor';
811import Mnemonic from './Mnemonic';
9−//TODO more
12+import PrivateKey from './PrivateKey';
13+import Keystore from './Keystore';
1014
1115import { ROUTE_NAMES } from '../../lib/routeNames';
1216import { BRIDGE_ERROR } from '../../lib/error';
@@ -70,24 +74,28 @@ class Advanced extends React.Component {
7074 }
7175
7276 render() {
77+ const loginCompleted = this.props.loginCompleted;
7378 const inputs = (() => {
7479 switch (this.state.selected) {
7580 case WALLET_TYPES.TICKET:
76− return (
77− <Ticket
78− advanced={true}
79− loginCompleted={this.props.loginCompleted}
80− />
81− );
81+ return <Ticket advanced={true} loginCompleted={loginCompleted} />;
82+ case WALLET_TYPES.SHARDS:
83+ return <Shards loginCompleted={loginCompleted} />;
84+ //
85+ case WALLET_TYPES.LEDGER:
86+ return <Ledger loginCompleted={loginCompleted} />;
87+ case WALLET_TYPES.TREZOR:
88+ return <Trezor loginCompleted={loginCompleted} />;
89+ //
8290 case WALLET_TYPES.MNEMONIC:
83− return (
84− <Mnemonic
85− advanced={true}
86− loginCompleted={this.props.loginCompleted}
87− />
88− );
91+ return <Mnemonic advanced={true} loginCompleted={loginCompleted} />;
92+ case WALLET_TYPES.PRIVATE_KEY:
93+ return <PrivateKey loginCompleted={loginCompleted} />;
94+ case WALLET_TYPES.KEYSTORE:
95+ return <Keystore loginCompleted={loginCompleted} />;
96+ //
8997 default:
90− return <P>{'Coming soon'}</P>;
98+ return <P>{'Coming soon.'}</P>;
9199 }
92100 })();
93101
src/bridge/views/Login/Keystore.jsnew file+167
@@ -0,0 +1,167 @@
1+import { Just, Nothing } from 'folktale/maybe';
2+import { Ok, Error } from 'folktale/result';
3+import React from 'react';
4+import * as keythereum from 'keythereum';
5+import { Button, UploadButton } from '../../components/Base';
6+import { Input, InnerLabel, InputCaption } from '../../components/Base';
7+import { Row, Col, H1, H3, Warning } from '../../components/Base';
8+
9+import { BRIDGE_ERROR } from '../../lib/error';
10+import { ROUTE_NAMES } from '../../lib/routeNames';
11+import { withHistory } from '../../store/history';
12+import { EthereumWallet } from '../../lib/wallet';
13+import { compose } from '../../lib/lib';
14+import { withWallet } from '../../store/wallet';
15+
16+class Keystore extends React.Component {
17+ constructor(props) {
18+ super(props);
19+
20+ this.state = {
21+ keystore: Nothing(), // Maybe<Result<String, String>>
22+ password: '',
23+ decryptionProblem: false,
24+ };
25+
26+ this.handleKeystoreUpload = this.handleKeystoreUpload.bind(this);
27+ this.handlePasswordInput = this.handlePasswordInput.bind(this);
28+ this.constructWallet = this.constructWallet.bind(this);
29+ }
30+
31+ handlePasswordInput(password) {
32+ this.setState({ password });
33+ }
34+
35+ constructWallet() {
36+ const { setWallet } = this.props;
37+ const { keystore, password } = this.state;
38+
39+ try {
40+ const text = keystore.matchWith({
41+ Nothing: _ => {
42+ throw BRIDGE_ERROR.MISSING_KEYSTORE;
43+ },
44+ Just: ks =>
45+ ks.value.matchWith({
46+ Ok: result => result.value,
47+ Error: _ => {
48+ throw BRIDGE_ERROR.MISSING_KEYSTORE;
49+ },
50+ }),
51+ });
52+
53+ const json = JSON.parse(text);
54+ const privateKey = keythereum.recover(password, json);
55+
56+ const wallet = new EthereumWallet(privateKey);
57+ this.setState({ decryptionProblem: false });
58+ setWallet(Just(wallet));
59+ } catch (err) {
60+ this.setState({ decryptionProblem: true });
61+ setWallet(Nothing());
62+ }
63+ }
64+
65+ handleKeystoreUpload = event => {
66+ const file = event.files.item(0);
67+ const reader = new FileReader();
68+
69+ reader.onload = e => {
70+ const keystore = e.target.result;
71+ this.setState({ keystore: Just(Ok(keystore)) });
72+ };
73+
74+ const failure = _ => {
75+ const message = 'There was a problem uploading your Keystore file';
76+ this.setState({ keystore: Just(Error(message)) });
77+ };
78+
79+ reader.onerror = failure;
80+ reader.onabort = failure;
81+
82+ reader.readAsText(file);
83+ };
84+
85+ render() {
86+ const { history, wallet } = this.props;
87+ const { keystore, password, decryptionProblem } = this.state;
88+
89+ const uploadButtonClass = keystore.matchWith({
90+ Nothing: _ => 'bg-blue white',
91+ Just: ks =>
92+ ks.value.matchWith({
93+ Ok: _ => 'bg-green white',
94+ Error: _ => 'bg-yellow black',
95+ }),
96+ });
97+
98+ const decryptMessage =
99+ decryptionProblem === false ? (
100+ <div />
101+ ) : (
102+ <Warning className={'mt-8'}>
103+ <H3 style={{ marginTop: 0, paddingTop: 0 }}>
104+ {"Couldn't decrypt wallet."}
105+ </H3>
106+ {'You may have entered an incorrect password.'}
107+ </Warning>
108+ );
109+
110+ return (
111+ <Row>
112+ <Col className={'measure-md'}>
113+ <H1 className={'mb-4'}>{'Upload Your Keystore File'}</H1>
114+ <InputCaption>
115+ {`Please upload your Ethereum keystore file. If your keystore
116+ file is encrypted with a password, you'll also need to enter
117+ that below.`}
118+ </InputCaption>
119+
120+ <UploadButton
121+ className={`btn ${uploadButtonClass} mt-10`}
122+ onChange={this.handleKeystoreUpload}>
123+ <div className={'flex-center-all fs-4 h-11 pointer'}>
124+ {'Upload Keystore file'}
125+ </div>
126+ </UploadButton>
127+
128+ <Input
129+ className="pt-8 mt-8"
130+ prop-size="md"
131+ prop-format="innerLabel"
132+ type="password"
133+ name="password"
134+ onChange={this.handlePasswordInput}
135+ value={password}
136+ autocomplete="off"
137+ autoFocus>
138+ <InnerLabel>{'Password'}</InnerLabel>
139+ </Input>
140+
141+ <Button
142+ className={` mt-10`}
143+ prop-size={'wide lg'}
144+ disabled={Nothing.hasInstance(keystore)}
145+ onClick={this.constructWallet}>
146+ {'Decrypt'}
147+ </Button>
148+
149+ {decryptMessage}
150+
151+ <Button
152+ className={'mt-10'}
153+ prop-size={'wide lg'}
154+ disabled={Nothing.hasInstance(wallet)}
155+ onClick={this.props.loginCompleted}>
156+ {'Continue →'}
157+ </Button>
158+ </Col>
159+ </Row>
160+ );
161+ }
162+}
163+
164+export default compose(
165+ withHistory,
166+ withWallet
167+)(Keystore);
src/bridge/views/Login/Ledger.jsnew file+225
@@ -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);
src/bridge/views/Login/PrivateKey.jsnew file+81
@@ -0,0 +1,81 @@
1+import Maybe from 'folktale/maybe';
2+import React from 'react';
3+import { Button } from '../../components/Base';
4+import { RequiredInput, InnerLabel, InputCaption } from '../../components/Base';
5+import { Row, Col, H1 } from '../../components/Base';
6+
7+import { ROUTE_NAMES } from '../../lib/routeNames';
8+import { withHistory } from '../../store/history';
9+import { EthereumWallet } from '../../lib/wallet';
10+import { compose } from '../../lib/lib';
11+import { withWallet } from '../../store/wallet';
12+
13+class PrivateKey extends React.Component {
14+ constructor(props) {
15+ super(props);
16+
17+ this.state = {
18+ privateKey: '',
19+ };
20+
21+ this.handlePrivateKeyInput = this.handlePrivateKeyInput.bind(this);
22+ }
23+
24+ handlePrivateKeyInput(privateKey) {
25+ this.setState({ privateKey });
26+ this.constructWallet(privateKey);
27+ }
28+
29+ constructWallet(privateKey) {
30+ const { setWallet } = this.props;
31+ if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
32+ const sec = Buffer.from(privateKey, 'hex');
33+ const wallet = new EthereumWallet(sec);
34+ setWallet(Maybe.Just(wallet));
35+ } else {
36+ setWallet(Maybe.Nothing());
37+ }
38+ }
39+
40+ render() {
41+ const { history, wallet } = this.props;
42+ const { privateKey } = this.state;
43+
44+ return (
45+ <Row>
46+ <Col className={'measure-md'}>
47+ <H1 className={'mb-4'}>{'Enter Your Private Key'}</H1>
48+ <InputCaption>
49+ {`Please enter your raw Ethereum private key here.`}
50+ </InputCaption>
51+
52+ <RequiredInput
53+ className="pt-8 mt-8"
54+ prop-size="md"
55+ prop-format="innerLabel"
56+ type="password"
57+ name="privateKey"
58+ onChange={this.handlePrivateKeyInput}
59+ value={privateKey}
60+ autocomplete="off"
61+ autoFocus>
62+ <InnerLabel>{'Private Key'}</InnerLabel>
63+ </RequiredInput>
64+
65+ <Button
66+ className={'mt-10'}
67+ prop-size={'wide lg'}
68+ disabled={Maybe.Nothing.hasInstance(wallet)}
69+ onClick={this.props.loginCompleted}>
70+ {'Continue →'}
71+ </Button>
72+ </Col>
73+ </Row>
74+ );
75+ }
76+}
77+
78+export default compose(
79+ withHistory,
80+ withWallet
81+)(PrivateKey);
src/bridge/views/Login/Shards.jsnew file+219
@@ -0,0 +1,219 @@
1+import { Just, Nothing } from 'folktale/maybe';
2+import React from 'react';
3+import { Button } from '../../components/Base';
4+import {
5+ InnerLabel,
6+ ValidatedSigil,
7+ PointInput,
8+ ShardInput,
9+ InputCaption,
10+ Input,
11+} from '../../components/Base';
12+import { Row, Col, H1, P } from '../../components/Base';
13+import * as kg from 'urbit-key-generation/dist/index';
14+import * as ob from 'urbit-ob';
15+
16+import { ROUTE_NAMES } from '../../lib/routeNames';
17+import { withHistory } from '../../store/history';
18+import { urbitWalletFromTicket } from '../../lib/wallet';
19+import { compose } from '../../lib/lib';
20+import { withWallet } from '../../store/wallet';
21+
22+const placeholder = len => {
23+ let bytes = window.crypto.getRandomValues(new Uint8Array(len));
24+ let hex = bytes.reduce(
25+ (acc, byt) => acc + byt.toString(16).padStart(2, '0'),
26+ ''
27+ );
28+ return ob.hex2patq(hex);
29+};
30+
31+const SHARDS = {
32+ SHARD1: Symbol('SHARD1'),
33+ SHARD2: Symbol('SHARD2'),
34+ SHARD3: Symbol('SHARD3'),
35+};
36+
37+class Shards extends React.Component {
38+ constructor(props) {
39+ super(props);
40+
41+ this.state = {
42+ shard1: '',
43+ shard2: '',
44+ shard3: '',
45+ passphrase: '',
46+ pointName: '',
47+ };
48+
49+ this.pointPlaceholder = placeholder(4);
50+ this.ticketPlaceholder = placeholder(16);
51+
52+ this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
53+ this.handleShardInput = this.handleShardInput.bind(this);
54+ this.handlePointNameInput = this.handlePointNameInput.bind(this);
55+ }
56+
57+ handleShardInput(shard, input) {
58+ if (shard === SHARDS.SHARD1) {
59+ this.setState({ shard1: input });
60+ } else if (shard === SHARDS.SHARD2) {
61+ this.setState({ shard2: input });
62+ } else if (shard === SHARDS.SHARD3) {
63+ this.setState({ shard3: input });
64+ }
65+ }
66+
67+ handlePassphraseInput(passphrase) {
68+ this.setState({ passphrase });
69+ }
70+
71+ handlePointNameInput(pointName) {
72+ if (pointName.length < 15) {
73+ this.setState({ pointName });
74+ }
75+ }
76+
77+ async walletFromShards(shard1, shard2, shard3, pointName, passphrase) {
78+ const s1 = shard1 === '' ? undefined : shard1;
79+ const s2 = shard2 === '' ? undefined : shard2;
80+ const s3 = shard3 === '' ? undefined : shard3;
81+
82+ let ticket = undefined;
83+ try {
84+ ticket = kg.combine([s1, s2, s3]);
85+ } catch (_) {
86+ // do nothing
87+ }
88+
89+ if (ticket !== undefined) {
90+ const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
91+ this.props.setUrbitWallet(Just(uhdw));
92+ }
93+ }
94+
95+ render() {
96+ const { history, wallet } = this.props;
97+ const { shard1, shard2, shard3, pointName, passphrase } = this.state;
98+
99+ const phPoint = this.pointPlaceholder;
100+ const phTick = this.ticketPlaceholder;
101+
102+ const shards = [shard1, shard2, shard3];
103+ const ready = shards.filter(x => x !== '').length > 1;
104+
105+ return (
106+ <Row>
107+ <Col>
108+ <H1>{'Authenticate'}</H1>
109+
110+ <P>
111+ {`Enter your point and at least two of your three Urbit master
112+ ticket shards here. The index of the input field needs to
113+ match the index of the shard.`}
114+ </P>
115+
116+ <PointInput
117+ className="mono mt-8"
118+ prop-size="lg"
119+ prop-format="innerLabel"
120+ type="text"
121+ autoFocus
122+ placeholder={`e.g. ${phPoint}`}
123+ value={pointName}
124+ onChange={this.handlePointNameInput}>
125+ <InnerLabel>{'Point'}</InnerLabel>
126+ <ValidatedSigil
127+ className={'tr-0 mt-05 mr-0 abs'}
128+ patp={pointName}
129+ size={68}
130+ margin={8}
131+ />
132+ </PointInput>
133+
134+ <ShardInput
135+ className="mono mt-8"
136+ prop-size="md"
137+ prop-format="innerLabel"
138+ type="text"
139+ name="shard1"
140+ placeholder={`e.g. ${phTick}`}
141+ value={shard1}
142+ onChange={inp => this.handleShardInput(SHARDS.SHARD1, inp)}>
143+ <InnerLabel>{'Shard 1'}</InnerLabel>
144+ </ShardInput>
145+
146+ <ShardInput
147+ className="mono mt-8"
148+ prop-size="md"
149+ prop-format="innerLabel"
150+ type="text"
151+ name="shard2"
152+ placeholder={`e.g. ${phTick}`}
153+ value={shard2}
154+ onChange={inp => this.handleShardInput(SHARDS.SHARD2, inp)}>
155+ <InnerLabel>{'Shard 2'}</InnerLabel>
156+ </ShardInput>
157+
158+ <ShardInput
159+ className="mono mt-8"
160+ prop-size="md"
161+ prop-format="innerLabel"
162+ type="text"
163+ name="shard3"
164+ placeholder={`e.g. ${phTick}`}
165+ value={shard3}
166+ onChange={inp => this.handleShardInput(SHARDS.SHARD3, inp)}>
167+ <InnerLabel>{'Shard 3'}</InnerLabel>
168+ </ShardInput>
169+
170+ <InputCaption>
171+ {'If your wallet requires a passphrase, you may enter it below.'}
172+ </InputCaption>
173+
174+ <Input
175+ className="pt-8"
176+ prop-size="md"
177+ prop-format="innerLabel"
178+ name="passphrase"
179+ type="password"
180+ value={passphrase}
181+ autocomplete="off"
182+ onChange={this.handlePassphraseInput}>
183+ <InnerLabel>{'Passphrase'}</InnerLabel>
184+ </Input>
185+
186+ <Button
187+ disabled={!ready}
188+ className={'mt-8'}
189+ prop-size={'lg wide'}
190+ // prop-color={this.buttonTriState(wallet)}
191+ onClick={() =>
192+ this.walletFromShards(
193+ shard1,
194+ shard2,
195+ shard3,
196+ pointName,
197+ passphrase
198+ )
199+ }>
200+ {'Unlock Wallet →'}
201+ </Button>
202+
203+ <Button
204+ className={'mt-4'}
205+ prop-size={'xl wide'}
206+ disabled={Nothing.hasInstance(wallet)}
207+ onClick={this.props.loginCompleted}>
208+ {'Continue →'}
209+ </Button>
210+ </Col>
211+ </Row>
212+ );
213+ }
214+}
215+
216+export default compose(
217+ withHistory,
218+ withWallet
219+)(Shards);
src/bridge/views/Login/Trezor.jsnew file+147
@@ -0,0 +1,147 @@
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 } from '../../components/Base';
6+import { InnerLabel, Input, InnerLabelDropdown } from '../../components/Base';
7+import TrezorConnect from 'trezor-connect';
8+import * as secp256k1 from 'secp256k1';
9+
10+import { TREZOR_PATH } from '../../lib/trezor';
11+import { ROUTE_NAMES } from '../../lib/routeNames';
12+import { withHistory } from '../../store/history';
13+import { compose } from '../../lib/lib';
14+import { withWallet } from '../../store/wallet';
15+
16+class Trezor extends React.Component {
17+ constructor(props) {
18+ super(props);
19+
20+ this.state = {
21+ hdpath: TREZOR_PATH.replace(/x/g, 0),
22+ account: 0,
23+ };
24+
25+ this.handleAccountSelection = this.handleAccountSelection.bind(this);
26+ this.handleHdPathInput = this.handleHdPathInput.bind(this);
27+ this.pollDevice = this.pollDevice.bind(this);
28+ }
29+
30+ handleAccountSelection(account) {
31+ let hdpath = this.state.hdpath;
32+ if (account !== 'custom') {
33+ hdpath = TREZOR_PATH.replace(/x/g, account);
34+ }
35+ this.setState({ account, hdpath });
36+ }
37+
38+ handleHdPathInput(hdpath) {
39+ this.setState({ hdpath });
40+ }
41+
42+ async pollDevice() {
43+ const { setWallet, setWalletHdPath } = this.props;
44+ const { hdpath } = this.state;
45+
46+ TrezorConnect.manifest({
47+ email: 'bridge-trezor@urbit.org',
48+ appUrl: 'https://github.com/urbit/bridge',
49+ });
50+ TrezorConnect.getPublicKey({ path: hdpath }).then(info => {
51+ if (info.success === true) {
52+ const payload = info.payload;
53+ const publicKey = Buffer.from(payload.publicKey, 'hex');
54+ const chainCode = Buffer.from(payload.chainCode, 'hex');
55+ const pub = secp256k1.publicKeyConvert(publicKey, true);
56+ const hd = bip32.fromPublicKey(pub, chainCode);
57+ setWallet(Maybe.Just(hd));
58+ setWalletHdPath(hdpath);
59+ } else {
60+ setWallet(Maybe.Nothing());
61+ }
62+ });
63+ }
64+
65+ render() {
66+ const { history, wallet } = this.props;
67+ const { hdpath, account } = this.state;
68+ const { handleAccountSelection } = this;
69+
70+ let accountOptions = [
71+ {
72+ title: 'Custom path',
73+ value: 'custom',
74+ },
75+ ];
76+ for (let i = 0; i < 20; i++) {
77+ accountOptions.push({
78+ title: 'Account #' + (i + 1),
79+ value: i,
80+ });
81+ }
82+ let accountTitle = accountOptions.find(o => o.value === account).title;
83+
84+ const accountSelection = (
85+ <InnerLabelDropdown
86+ className="mt-8"
87+ prop-size="md"
88+ prop-format="innerLabel"
89+ options={accountOptions}
90+ handleUpdate={handleAccountSelection}
91+ title={'Account'}
92+ currentSelectionTitle={accountTitle}
93+ fullWidth={true}
94+ />
95+ );
96+
97+ const pathSelection =
98+ account !== 'custom' ? null : (
99+ <Input
100+ className="mt-8 pt-8 text-mono"
101+ prop-size="md"
102+ prop-format="innerLabel"
103+ name="hdpath"
104+ value={hdpath}
105+ autocomplete="off"
106+ onChange={this.handleHdPathInput}>
107+ <InnerLabel>{'HD Path'}</InnerLabel>
108+ </Input>
109+ );
110+
111+ return (
112+ <Row>
113+ <Col className={'measure-md'}>
114+ <H1>{'Authenticate With Your Trezor'}</H1>
115+
116+ <P>
117+ {`Connect and authenticate to your Trezor. If you'd like
118+ to use a custom derivation path, you may enter it below.`}
119+ </P>
120+
121+ {accountSelection}
122+ {pathSelection}
123+
124+ <Button
125+ className={'mt-8'}
126+ prop-size={'wide lg'}
127+ onClick={this.pollDevice}>
128+ {'Authenticate →'}
129+ </Button>
130+
131+ <Button
132+ className={'mt-8'}
133+ prop-size={'wide lg'}
134+ disabled={Maybe.Nothing.hasInstance(wallet)}
135+ onClick={this.props.loginCompleted}>
136+ {'Continue →'}
137+ </Button>
138+ </Col>
139+ </Row>
140+ );
141+ }
142+}
143+
144+export default compose(
145+ withHistory,
146+ withWallet
147+)(Trezor);