on master help sign in
Merge branch 'mino' into feat/indigo
commitb4917dfed3b4535188cdd871620f0610c18126a9 authorFang <git@fang.io> dateFri 21 Jun 2019, 00:28 (7 years ago) parent7f633060e91f6a
18 files changed+1576−4
package-lock.json+3−3 package.json+1−1 src/components/Tabs.js+18 src/lib/need.js+9 src/lib/routeNames.js+3 src/lib/router.js+8 src/store/wallet.js+2 src/views/Landing.js+7 src/views/Login.js+122 src/views/Login/Advanced.js+115 src/views/Login/Keystore.js+151 src/views/Login/Ledger.js+224 src/views/Login/Mnemonic.js+160 src/views/Login/PrivateKey.js+80 src/views/Login/Shards.js+214 src/views/Login/Ticket.js+202 src/views/Login/Trezor.js+146 src/views/PointHome.js+111
package-lock.json+3−3
@@ -2638,9 +2638,9 @@
26382638 }
26392639 },
26402640 "azimuth-js": {
2641− "version": "0.14.2",
2642− "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.14.2.tgz",
2643− "integrity": "sha512-54CJ8K0dheso9U1sBWCHkMvUj7/EIEDGc/UH1HU2uenEkSQpx9Ic4jHq9H6W8baew//PEH4ldYnFFVfsd4Gi4Q==",
2641+ "version": "0.15.0",
2642+ "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.15.0.tgz",
2643+ "integrity": "sha512-U/700d/94vvjDXpfXVPhrmYna3EX4aVRfkAdG21+k54LJTEozKAQySTPiuOpNsn9kEjaAp3sHR+C9glxm+5NEg==",
26442644 "requires": {
26452645 "azimuth-solidity": "1.2.0",
26462646 "ethereumjs-util": "^5.2.0"
package.json+1−1
@@ -17,7 +17,7 @@
1717 "@ledgerhq/hw-app-eth": "^4.61.0",
1818 "@ledgerhq/hw-transport-u2f": "^4.61.0",
1919 "PaperCollateralRenderer": "github:urbit/PaperCollateralRenderer",
20− "azimuth-js": "^0.14.0",
20+ "azimuth-js": "^0.15.0",
2121 "azimuth-solidity": "^1.2.0",
2222 "bip32": "^1.0.2",
2323 "bip39": "^2.5.0",
src/components/Tabs.jsnew file+18
@@ -0,0 +1,18 @@
1+import React from 'react';
2+import HorizontalSelector from './old/HorizontalSelector';
3+
4+export default function Tabs({
5+ tabViews,
6+ tabOptions,
7+ currentTab,
8+ onTabChange,
9+ ...rest
10+}) {
11+ const Component = tabViews[currentTab];
12+ return (
13+ <>
14+ <HorizontalSelector options={tabOptions} onChange={onTabChange} />
15+ <Component {...rest} />
16+ </>
17+ );
18+}
src/lib/need.js+9
@@ -30,3 +30,12 @@ export const fromPointCache = (obj, point) => {
3030
3131 return cache[point];
3232};
33+export const keystore = obj => {
34+ const ks = needBuilder(BRIDGE_ERROR.MISSING_KEYSTORE)();
35+ return ks.value.matchWith({
36+ Ok: result => result.value,
37+ Error: _ => {
38+ throw BRIDGE_ERROR.MISSING_KEYSTORE;
39+ },
40+ });
41+};
src/lib/routeNames.js+3
@@ -6,6 +6,9 @@ export const ROUTE_NAMES = {
66 INVITE: Symbol('INVITE'),
77 INVITES_MANAGE: Symbol('INVITES_MANAGE'),
88 //
9+ LOGIN: Symbol('LOGIN'),
10+ POINT_HOME: Symbol('POINT_HOME'),
11+ //
912 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
1013 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
1114 LANDING: Symbol('LANDING'),
src/lib/router.js+8
@@ -5,6 +5,8 @@ import * as need from './need';
55import Landing from '../views/Landing';
66import InviteTicket from '../views/InviteTicket';
77import InvitesManage from '../views/InvitesManage.js';
8+import Login from '../views/Login.js';
9+import PointHome from '../views/PointHome.js';
810import AcceptTransfer from '../views/AcceptTransfer';
911import CancelTransfer from '../views/CancelTransfer';
1012import CreateGalaxy from '../views/CreateGalaxy';
@@ -42,6 +44,8 @@ export const ROUTES = {
4244 [ROUTE_NAMES.INVITE_TICKET]: InviteTicket,
4345 [ROUTE_NAMES.INVITE]: Invite,
4446 [ROUTE_NAMES.INVITES_MANAGE]: InvitesManage,
47+ [ROUTE_NAMES.LOGIN]: Login,
48+ [ROUTE_NAMES.POINT_HOME]: PointHome,
4549 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
4650 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
4751 [ROUTE_NAMES.NETWORK]: Network,
@@ -81,6 +85,10 @@ export const getRouteBreadcrumb = ({
8185 return 'Send invites';
8286 case ROUTE_NAMES.INVITES_MANAGE:
8387 return 'Manage invites';
88+ case ROUTE_NAMES.LOGIN:
89+ return 'Login to Bridge';
90+ case ROUTE_NAMES.POINT_HOME:
91+ return 'Overview: ' + ob.patp(need.pointCursor({ pointCursor }));
8492 case ROUTE_NAMES.NETWORK:
8593 return renderNetworkType(networkType);
8694 case ROUTE_NAMES.WALLET:
src/store/wallet.js+2
@@ -84,6 +84,8 @@ function _useWallet(
8484 });
8585
8686 setWallet(wallet);
87+ } else {
88+ setWallet(Maybe.Nothing());
8789 }
8890
8991 _setUrbitWallet(urbitWallet);
src/views/Landing.js+7
@@ -26,6 +26,13 @@ function Landing(props) {
2626 Claim an invite
2727 </ForwardButton>
2828
29+ <ForwardButton
30+ className="mt3"
31+ detail="Shiny new Login flow."
32+ onClick={() => history.push(ROUTE_NAMES.LOGIN)}>
33+ Login
34+ </ForwardButton>
35+
2936 <ForwardButton
3037 className="mt3"
3138 detail="If you own Azimuth assets and want to manage them in some way."
src/views/Login.jsnew file+122
@@ -0,0 +1,122 @@
1+import { Nothing, Just } from 'folktale/maybe';
2+import React from 'react';
3+import * as azimuth from 'azimuth-js';
4+
5+import { H1, Row, Col } from '../components/old/Base';
6+import Tabs from '../components/Tabs';
7+
8+import Ticket from './Login/Ticket';
9+import Mnemonic from './Login/Mnemonic';
10+import Advanced from './Login/Advanced';
11+
12+import * as need from '../lib/need';
13+import { compose } from '../lib/lib';
14+import { ROUTE_NAMES } from '../lib/routeNames';
15+
16+import { withHistory } from '../store/history';
17+import { withNetwork } from '../store/network';
18+import { withWallet } from '../store/wallet';
19+import { withPointCursor } from '../store/pointCursor';
20+
21+const TABS = {
22+ TICKET: Symbol('TICKET'),
23+ MNEMONIC: Symbol('MNEMONIC'),
24+ ADVANCED: Symbol('ADVANCED'),
25+};
26+
27+class Login extends React.Component {
28+ constructor(props) {
29+ super(props);
30+
31+ this.state = {
32+ currentTab: TABS.TICKET,
33+ };
34+
35+ this.handleTabChange = this.handleTabChange.bind(this);
36+ this.continue = this.continue.bind(this);
37+ }
38+
39+ componentDidMount() {
40+ // we expect wallet and pointCursor to not be set yet
41+ this.props.setUrbitWallet(Nothing());
42+ this.props.setWallet(Nothing());
43+ this.props.setPointCursor(Nothing());
44+ }
45+
46+ handleTabChange(currentTab) {
47+ this.setState({ currentTab });
48+ }
49+
50+ async continue() {
51+ const wallet = need.wallet(this.props);
52+ const contracts = need.contracts(this.props);
53+
54+ // if no point cursor set by login logic, try to deduce it
55+ let deduced = Nothing();
56+ if (Nothing.hasInstance(this.props.pointCursor)) {
57+ const owned = await azimuth.azimuth.getOwnedPoints(
58+ contracts,
59+ wallet.address
60+ );
61+ if (owned.length === 1) {
62+ deduced = Just(owned[0]);
63+ } else if (owned.length === 0) {
64+ const canOwn = await azimuth.azimuth.getTransferringFor(
65+ contracts,
66+ wallet.address
67+ );
68+ if (canOwn.length === 1) {
69+ deduced = Just(canOwn[0]);
70+ }
71+ }
72+ }
73+
74+ if (Just.hasInstance(deduced)) {
75+ this.props.setPointCursor(deduced);
76+ this.props.history.popAndPush(ROUTE_NAMES.POINT_HOME);
77+ } else if (Just.hasInstance(this.props.pointCursor)) {
78+ this.props.history.popAndPush(ROUTE_NAMES.POINT_HOME);
79+ } else {
80+ //TODO to new overview (maybe should be merged into point home?)
81+ this.props.history.popAndPush(ROUTE_NAMES.SHIPS);
82+ }
83+ }
84+
85+ render() {
86+ const tabOptions = [
87+ { title: 'Ticket', value: TABS.TICKET },
88+ { title: 'Mnemonic', value: TABS.MNEMONIC },
89+ { title: 'Advanced', value: TABS.ADVANCED },
90+ ];
91+
92+ const tabViews = {
93+ [TABS.TICKET]: Ticket,
94+ [TABS.MNEMONIC]: Mnemonic,
95+ [TABS.ADVANCED]: Advanced,
96+ };
97+
98+ return (
99+ <Row>
100+ <Col>
101+ <H1>{'Login'}</H1>
102+
103+ <Tabs
104+ tabViews={tabViews}
105+ tabOptions={tabOptions}
106+ currentTab={this.state.currentTab}
107+ onTabChange={this.handleTabChange}
108+ //
109+ loginCompleted={this.continue}
110+ />
111+ </Col>
112+ </Row>
113+ );
114+ }
115+}
116+
117+export default compose(
118+ withHistory,
119+ withNetwork,
120+ withWallet,
121+ withPointCursor
122+)(Login);
src/views/Login/Advanced.jsnew file+115
@@ -0,0 +1,115 @@
1+import React from 'react';
2+import { P, InnerLabelDropdown } from '../../components/old/Base';
3+import { Row, Col } from '../../components/old/Base';
4+
5+import Ticket from './Ticket';
6+import Shards from './Shards';
7+import Ledger from './Ledger';
8+import Trezor from './Trezor';
9+import Mnemonic from './Mnemonic';
10+import PrivateKey from './PrivateKey';
11+import Keystore from './Keystore';
12+
13+import { WALLET_TYPES, renderWalletType } from '../../lib/wallet';
14+import { compose } from '../../lib/lib';
15+import { withWallet } from '../../store/wallet';
16+
17+class Advanced extends React.Component {
18+ constructor(props) {
19+ super(props);
20+
21+ this.state = {
22+ selected: WALLET_TYPES.TICKET,
23+ };
24+
25+ this.handleSelection = this.handleSelection.bind(this);
26+ }
27+
28+ getWalletOptions() {
29+ return [
30+ {
31+ title: 'Urbit Master Ticket',
32+ value: WALLET_TYPES.TICKET,
33+ },
34+ {
35+ title: 'Urbit Master Ticket (Shards)',
36+ value: WALLET_TYPES.SHARDS,
37+ },
38+ {
39+ type: 'divider',
40+ },
41+ {
42+ title: 'Ledger',
43+ value: WALLET_TYPES.LEDGER,
44+ },
45+ {
46+ title: 'Trezor',
47+ value: WALLET_TYPES.TREZOR,
48+ },
49+ {
50+ type: 'divider',
51+ },
52+ {
53+ title: 'BIP39 Mnemonic',
54+ value: WALLET_TYPES.MNEMONIC,
55+ },
56+ {
57+ title: 'Ethereum Private Key',
58+ value: WALLET_TYPES.PRIVATE_KEY,
59+ },
60+ {
61+ title: 'Ethereum Keystore File',
62+ value: WALLET_TYPES.KEYSTORE,
63+ },
64+ ];
65+ }
66+
67+ handleSelection(selected) {
68+ this.setState({ selected });
69+ }
70+
71+ render() {
72+ const loginCompleted = this.props.loginCompleted;
73+ const inputs = (() => {
74+ switch (this.state.selected) {
75+ case WALLET_TYPES.TICKET:
76+ return <Ticket advanced={true} loginCompleted={loginCompleted} />;
77+ case WALLET_TYPES.SHARDS:
78+ return <Shards loginCompleted={loginCompleted} />;
79+ //
80+ case WALLET_TYPES.LEDGER:
81+ return <Ledger loginCompleted={loginCompleted} />;
82+ case WALLET_TYPES.TREZOR:
83+ return <Trezor loginCompleted={loginCompleted} />;
84+ //
85+ case WALLET_TYPES.MNEMONIC:
86+ return <Mnemonic advanced={true} loginCompleted={loginCompleted} />;
87+ case WALLET_TYPES.PRIVATE_KEY:
88+ return <PrivateKey loginCompleted={loginCompleted} />;
89+ case WALLET_TYPES.KEYSTORE:
90+ return <Keystore loginCompleted={loginCompleted} />;
91+ //
92+ default:
93+ return <P>{'Coming soon.'}</P>;
94+ }
95+ })();
96+
97+ return (
98+ <Row>
99+ <Col>
100+ <InnerLabelDropdown
101+ className={'mb-10 mt-6'}
102+ title={'Wallet Type:'}
103+ options={this.getWalletOptions()}
104+ handleUpdate={this.handleSelection}
105+ currentSelectionTitle={renderWalletType(this.state.selected)}
106+ />
107+
108+ {inputs}
109+ </Col>
110+ </Row>
111+ );
112+ }
113+}
114+
115+export default compose(withWallet)(Advanced);
src/views/Login/Keystore.jsnew file+151
@@ -0,0 +1,151 @@
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/old/Base';
6+import { Input, InnerLabel, InputCaption } from '../../components/old/Base';
7+import { Row, Col, H1, H3, Warning } from '../../components/old/Base';
8+
9+import * as need from '../../lib/need';
10+import { EthereumWallet } from '../../lib/wallet';
11+import { compose } from '../../lib/lib';
12+import { withWallet } from '../../store/wallet';
13+
14+class Keystore extends React.Component {
15+ constructor(props) {
16+ super(props);
17+
18+ this.state = {
19+ keystore: Nothing(), // Maybe<Result<String, String>>
20+ password: '',
21+ decryptionProblem: false,
22+ };
23+
24+ this.handleKeystoreUpload = this.handleKeystoreUpload.bind(this);
25+ this.handlePasswordInput = this.handlePasswordInput.bind(this);
26+ this.constructWallet = this.constructWallet.bind(this);
27+ }
28+
29+ handlePasswordInput(password) {
30+ this.setState({ password });
31+ }
32+
33+ constructWallet() {
34+ const { setWallet } = this.props;
35+ const { password } = this.state;
36+
37+ try {
38+ const text = need.keystore(this.state);
39+
40+ const json = JSON.parse(text);
41+ const privateKey = keythereum.recover(password, json);
42+
43+ const wallet = new EthereumWallet(privateKey);
44+ this.setState({ decryptionProblem: false });
45+ setWallet(Just(wallet));
46+ } catch (err) {
47+ this.setState({ decryptionProblem: true });
48+ setWallet(Nothing());
49+ }
50+ }
51+
52+ handleKeystoreUpload = event => {
53+ const file = event.files.item(0);
54+ const reader = new FileReader();
55+
56+ reader.onload = e => {
57+ const keystore = e.target.result;
58+ this.setState({ keystore: Just(Ok(keystore)) });
59+ };
60+
61+ const failure = _ => {
62+ const message = 'There was a problem uploading your Keystore file';
63+ this.setState({ keystore: Just(Error(message)) });
64+ };
65+
66+ reader.onerror = failure;
67+ reader.onabort = failure;
68+
69+ reader.readAsText(file);
70+ };
71+
72+ render() {
73+ const { wallet } = this.props;
74+ const { keystore, password, decryptionProblem } = this.state;
75+
76+ const uploadButtonClass = keystore.matchWith({
77+ Nothing: _ => 'bg-blue white',
78+ Just: ks =>
79+ ks.value.matchWith({
80+ Ok: _ => 'bg-green white',
81+ Error: _ => 'bg-yellow black',
82+ }),
83+ });
84+
85+ const decryptMessage =
86+ decryptionProblem === false ? (
87+ <div />
88+ ) : (
89+ <Warning className={'mt-8'}>
90+ <H3 style={{ marginTop: 0, paddingTop: 0 }}>
91+ {"Couldn't decrypt wallet."}
92+ </H3>
93+ {'You may have entered an incorrect password.'}
94+ </Warning>
95+ );
96+
97+ return (
98+ <Row>
99+ <Col className={'measure-md'}>
100+ <H1 className={'mb-4'}>{'Upload Your Keystore File'}</H1>
101+ <InputCaption>
102+ {`Please upload your Ethereum keystore file. If your keystore
103+ file is encrypted with a password, you'll also need to enter
104+ that below.`}
105+ </InputCaption>
106+
107+ <UploadButton
108+ className={`btn ${uploadButtonClass} mt-10`}
109+ onChange={this.handleKeystoreUpload}>
110+ <div className={'flex-center-all fs-4 h-11 pointer'}>
111+ {'Upload Keystore file'}
112+ </div>
113+ </UploadButton>
114+
115+ <Input
116+ className="pt-8 mt-8"
117+ prop-size="md"
118+ prop-format="innerLabel"
119+ type="password"
120+ name="password"
121+ onChange={this.handlePasswordInput}
122+ value={password}
123+ autocomplete="off"
124+ autoFocus>
125+ <InnerLabel>{'Password'}</InnerLabel>
126+ </Input>
127+
128+ <Button
129+ className={` mt-10`}
130+ prop-size={'wide lg'}
131+ disabled={Nothing.hasInstance(keystore)}
132+ onClick={this.constructWallet}>
133+ {'Decrypt'}
134+ </Button>
135+
136+ {decryptMessage}
137+
138+ <Button
139+ className={'mt-10'}
140+ prop-size={'wide lg'}
141+ disabled={Nothing.hasInstance(wallet)}
142+ onClick={this.props.loginCompleted}>
143+ {'Continue →'}
144+ </Button>
145+ </Col>
146+ </Row>
147+ );
148+ }
149+}
150+
151+export default compose(withWallet)(Keystore);
src/views/Login/Ledger.jsnew file+224
@@ -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);
src/views/Login/Mnemonic.jsnew file+160
@@ -0,0 +1,160 @@
1+import { Just, Nothing } from 'folktale/maybe';
2+import React from 'react';
3+import { Button } from '../../components/old/Base';
4+import {
5+ Input,
6+ MnemonicInput,
7+ InnerLabel,
8+ InputCaption,
9+} from '../../components/old/Base';
10+import { Row, Col } from '../../components/old/Base';
11+
12+import { DEFAULT_HD_PATH, walletFromMnemonic } from '../../lib/wallet';
13+import { compose } from '../../lib/lib';
14+import { withWallet } from '../../store/wallet';
15+
16+class Mnemonic extends React.Component {
17+ constructor(props) {
18+ super(props);
19+
20+ this.state = {
21+ mnemonic: '',
22+ passphrase: '',
23+ hdpath: DEFAULT_HD_PATH,
24+ };
25+
26+ this.handleMnemonicInput = this.handleMnemonicInput.bind(this);
27+ this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
28+ this.handleHdPathInput = this.handleHdPathInput.bind(this);
29+ }
30+
31+ componentDidMount() {
32+ const { mnemonic, hdpath, passphrase } = this.state;
33+ this.attemptWalletDerivation(mnemonic, hdpath, passphrase);
34+ }
35+
36+ handlePassphraseInput(passphrase) {
37+ this.setState((state, _) => {
38+ const mnemonic = state.mnemonic;
39+ const hdpath = state.hdpath;
40+ this.attemptWalletDerivation(
41+ mnemonic,
42+ hdpath === '' ? DEFAULT_HD_PATH : hdpath,
43+ passphrase
44+ );
45+ return {
46+ passphrase,
47+ };
48+ });
49+ }
50+
51+ handleMnemonicInput(mnemonic) {
52+ this.setState((state, _) => {
53+ const hdpath = state.hdpath;
54+ const passphrase = state.passphrase;
55+ this.attemptWalletDerivation(
56+ mnemonic,
57+ hdpath === '' ? DEFAULT_HD_PATH : hdpath,
58+ passphrase
59+ );
60+ return {
61+ mnemonic,
62+ };
63+ });
64+ }
65+
66+ handleHdPathInput(hdpath) {
67+ this.setState((state, _) => {
68+ const mnemonic = state.mnemonic;
69+ const passphrase = state.passphrase;
70+ this.attemptWalletDerivation(
71+ mnemonic,
72+ hdpath === '' ? DEFAULT_HD_PATH : hdpath,
73+ passphrase
74+ );
75+ return {
76+ hdpath,
77+ };
78+ });
79+ }
80+
81+ attemptWalletDerivation(mnemonic, hdpath, passphrase) {
82+ const { setWallet, setAuthMnemonic, setWalletHdPath } = this.props;
83+ const wallet = walletFromMnemonic(mnemonic, hdpath, passphrase);
84+ setWallet(wallet);
85+ setAuthMnemonic(Just(mnemonic));
86+ setWalletHdPath(hdpath);
87+ }
88+
89+ render() {
90+ const { wallet } = this.props;
91+ const { mnemonic, hdpath, passphrase } = this.state;
92+
93+ return (
94+ <Row>
95+ <Col>
96+ <InputCaption>
97+ {'Please enter your BIP39 mnemonic here.'}
98+ </InputCaption>
99+
100+ <MnemonicInput
101+ className="pt-8"
102+ prop-size="md"
103+ prop-format="innerLabel"
104+ type="text"
105+ name="mnemonic"
106+ onChange={this.handleMnemonicInput}
107+ value={mnemonic}
108+ autocomplete="off"
109+ autoFocus>
110+ <InnerLabel>{'Mnemonic'}</InnerLabel>
111+ </MnemonicInput>
112+
113+ {!this.props.advanced ? null : (
114+ <>
115+ <InputCaption>
116+ {`If your wallet requires a passphrase, you may enter it below.`}
117+ </InputCaption>
118+
119+ <Input
120+ className="pt-8"
121+ prop-size="md"
122+ prop-format="innerLabel"
123+ name="passphrase"
124+ type="password"
125+ value={passphrase}
126+ autocomplete="off"
127+ onChange={this.handlePassphraseInput}>
128+ <InnerLabel>{'Passphrase'}</InnerLabel>
129+ </Input>
130+
131+ <InputCaption>
132+ {`If you'd like to use a custom derivation path, you may enter it below.`}
133+ </InputCaption>
134+
135+ <Input
136+ className="pt-8 text-mono"
137+ prop-size="md"
138+ prop-format="innerLabel"
139+ name="hdpath"
140+ value={hdpath}
141+ autocomplete="off"
142+ onChange={this.handleHdPathInput}>
143+ <InnerLabel>{'HD Path'}</InnerLabel>
144+ </Input>
145+ </>
146+ )}
147+
148+ <Button
149+ className={'mt-10'}
150+ disabled={Nothing.hasInstance(wallet)}
151+ onClick={this.props.loginCompleted}>
152+ {'Go →'}
153+ </Button>
154+ </Col>
155+ </Row>
156+ );
157+ }
158+}
159+
160+export default compose(withWallet)(Mnemonic);
src/views/Login/PrivateKey.jsnew file+80
@@ -0,0 +1,80 @@
1+import Maybe from 'folktale/maybe';
2+import React from 'react';
3+import { Button } from '../../components/old/Base';
4+import {
5+ RequiredInput,
6+ InnerLabel,
7+ InputCaption,
8+} from '../../components/old/Base';
9+import { Row, Col, H1 } from '../../components/old/Base';
10+
11+import { EthereumWallet } from '../../lib/wallet';
12+import { compose } from '../../lib/lib';
13+import { withWallet } from '../../store/wallet';
14+
15+class PrivateKey extends React.Component {
16+ constructor(props) {
17+ super(props);
18+
19+ this.state = {
20+ privateKey: '',
21+ };
22+
23+ this.handlePrivateKeyInput = this.handlePrivateKeyInput.bind(this);
24+ }
25+
26+ handlePrivateKeyInput(privateKey) {
27+ this.setState({ privateKey });
28+ this.constructWallet(privateKey);
29+ }
30+
31+ constructWallet(privateKey) {
32+ const { setWallet } = this.props;
33+ if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
34+ const sec = Buffer.from(privateKey, 'hex');
35+ const wallet = new EthereumWallet(sec);
36+ setWallet(Maybe.Just(wallet));
37+ } else {
38+ setWallet(Maybe.Nothing());
39+ }
40+ }
41+
42+ render() {
43+ const { wallet } = this.props;
44+ const { privateKey } = this.state;
45+
46+ return (
47+ <Row>
48+ <Col className={'measure-md'}>
49+ <H1 className={'mb-4'}>{'Enter Your Private Key'}</H1>
50+ <InputCaption>
51+ {`Please enter your raw Ethereum private key here.`}
52+ </InputCaption>
53+
54+ <RequiredInput
55+ className="pt-8 mt-8"
56+ prop-size="md"
57+ prop-format="innerLabel"
58+ type="password"
59+ name="privateKey"
60+ onChange={this.handlePrivateKeyInput}
61+ value={privateKey}
62+ autocomplete="off"
63+ autoFocus>
64+ <InnerLabel>{'Private Key'}</InnerLabel>
65+ </RequiredInput>
66+
67+ <Button
68+ className={'mt-10'}
69+ prop-size={'wide lg'}
70+ disabled={Maybe.Nothing.hasInstance(wallet)}
71+ onClick={this.props.loginCompleted}>
72+ {'Continue →'}
73+ </Button>
74+ </Col>
75+ </Row>
76+ );
77+ }
78+}
79+
80+export default compose(withWallet)(PrivateKey);
src/views/Login/Shards.jsnew file+214
@@ -0,0 +1,214 @@
1+import { Just, Nothing } from 'folktale/maybe';
2+import React from 'react';
3+import { Button } from '../../components/old/Base';
4+import {
5+ InnerLabel,
6+ ValidatedSigil,
7+ PointInput,
8+ ShardInput,
9+ InputCaption,
10+ Input,
11+} from '../../components/old/Base';
12+import { Row, Col, H1, P } from '../../components/old/Base';
13+import * as kg from 'urbit-key-generation/dist/index';
14+import * as ob from 'urbit-ob';
15+
16+import { urbitWalletFromTicket } from '../../lib/wallet';
17+import { compose } from '../../lib/lib';
18+import { withWallet } from '../../store/wallet';
19+
20+const placeholder = len => {
21+ let bytes = window.crypto.getRandomValues(new Uint8Array(len));
22+ let hex = bytes.reduce(
23+ (acc, byt) => acc + byt.toString(16).padStart(2, '0'),
24+ ''
25+ );
26+ return ob.hex2patq(hex);
27+};
28+
29+const SHARDS = {
30+ SHARD1: Symbol('SHARD1'),
31+ SHARD2: Symbol('SHARD2'),
32+ SHARD3: Symbol('SHARD3'),
33+};
34+
35+class Shards extends React.Component {
36+ constructor(props) {
37+ super(props);
38+
39+ this.state = {
40+ shard1: '',
41+ shard2: '',
42+ shard3: '',
43+ passphrase: '',
44+ pointName: '',
45+ };
46+
47+ this.pointPlaceholder = placeholder(4);
48+ this.ticketPlaceholder = placeholder(16);
49+
50+ this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
51+ this.handleShardInput = this.handleShardInput.bind(this);
52+ this.handlePointNameInput = this.handlePointNameInput.bind(this);
53+ }
54+
55+ handleShardInput(shard, input) {
56+ if (shard === SHARDS.SHARD1) {
57+ this.setState({ shard1: input });
58+ } else if (shard === SHARDS.SHARD2) {
59+ this.setState({ shard2: input });
60+ } else if (shard === SHARDS.SHARD3) {
61+ this.setState({ shard3: input });
62+ }
63+ }
64+
65+ handlePassphraseInput(passphrase) {
66+ this.setState({ passphrase });
67+ }
68+
69+ handlePointNameInput(pointName) {
70+ if (pointName.length < 15) {
71+ this.setState({ pointName });
72+ }
73+ }
74+
75+ async walletFromShards(shard1, shard2, shard3, pointName, passphrase) {
76+ const s1 = shard1 === '' ? undefined : shard1;
77+ const s2 = shard2 === '' ? undefined : shard2;
78+ const s3 = shard3 === '' ? undefined : shard3;
79+
80+ let ticket = undefined;
81+ try {
82+ ticket = kg.combine([s1, s2, s3]);
83+ } catch (_) {
84+ // do nothing
85+ }
86+
87+ if (ticket !== undefined) {
88+ const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
89+ this.props.setUrbitWallet(Just(uhdw));
90+ }
91+ }
92+
93+ render() {
94+ const { wallet } = this.props;
95+ const { shard1, shard2, shard3, pointName, passphrase } = this.state;
96+
97+ const phPoint = this.pointPlaceholder;
98+ const phTick = this.ticketPlaceholder;
99+
100+ const shards = [shard1, shard2, shard3];
101+ const ready = shards.filter(x => x !== '').length > 1;
102+
103+ return (
104+ <Row>
105+ <Col>
106+ <H1>{'Authenticate'}</H1>
107+
108+ <P>
109+ {`Enter your point and at least two of your three Urbit master
110+ ticket shards here. The index of the input field needs to
111+ match the index of the shard.`}
112+ </P>
113+
114+ <PointInput
115+ className="mono mt-8"
116+ prop-size="lg"
117+ prop-format="innerLabel"
118+ type="text"
119+ autoFocus
120+ placeholder={`e.g. ${phPoint}`}
121+ value={pointName}
122+ onChange={this.handlePointNameInput}>
123+ <InnerLabel>{'Point'}</InnerLabel>
124+ <ValidatedSigil
125+ className={'tr-0 mt-05 mr-0 abs'}
126+ patp={pointName}
127+ size={68}
128+ margin={8}
129+ />
130+ </PointInput>
131+
132+ <ShardInput
133+ className="mono mt-8"
134+ prop-size="md"
135+ prop-format="innerLabel"
136+ type="text"
137+ name="shard1"
138+ placeholder={`e.g. ${phTick}`}
139+ value={shard1}
140+ onChange={inp => this.handleShardInput(SHARDS.SHARD1, inp)}>
141+ <InnerLabel>{'Shard 1'}</InnerLabel>
142+ </ShardInput>
143+
144+ <ShardInput
145+ className="mono mt-8"
146+ prop-size="md"
147+ prop-format="innerLabel"
148+ type="text"
149+ name="shard2"
150+ placeholder={`e.g. ${phTick}`}
151+ value={shard2}
152+ onChange={inp => this.handleShardInput(SHARDS.SHARD2, inp)}>
153+ <InnerLabel>{'Shard 2'}</InnerLabel>
154+ </ShardInput>
155+
156+ <ShardInput
157+ className="mono mt-8"
158+ prop-size="md"
159+ prop-format="innerLabel"
160+ type="text"
161+ name="shard3"
162+ placeholder={`e.g. ${phTick}`}
163+ value={shard3}
164+ onChange={inp => this.handleShardInput(SHARDS.SHARD3, inp)}>
165+ <InnerLabel>{'Shard 3'}</InnerLabel>
166+ </ShardInput>
167+
168+ <InputCaption>
169+ {'If your wallet requires a passphrase, you may enter it below.'}
170+ </InputCaption>
171+
172+ <Input
173+ className="pt-8"
174+ prop-size="md"
175+ prop-format="innerLabel"
176+ name="passphrase"
177+ type="password"
178+ value={passphrase}
179+ autocomplete="off"
180+ onChange={this.handlePassphraseInput}>
181+ <InnerLabel>{'Passphrase'}</InnerLabel>
182+ </Input>
183+
184+ <Button
185+ disabled={!ready}
186+ className={'mt-8'}
187+ prop-size={'lg wide'}
188+ // prop-color={this.buttonTriState(wallet)}
189+ onClick={() =>
190+ this.walletFromShards(
191+ shard1,
192+ shard2,
193+ shard3,
194+ pointName,
195+ passphrase
196+ )
197+ }>
198+ {'Unlock Wallet →'}
199+ </Button>
200+
201+ <Button
202+ className={'mt-4'}
203+ prop-size={'xl wide'}
204+ disabled={Nothing.hasInstance(wallet)}
205+ onClick={this.props.loginCompleted}>
206+ {'Continue →'}
207+ </Button>
208+ </Col>
209+ </Row>
210+ );
211+ }
212+}
213+
214+export default compose(withWallet)(Shards);
src/views/Login/Ticket.jsnew file+202
@@ -0,0 +1,202 @@
1+import { Just, Nothing } from 'folktale/maybe';
2+import React from 'react';
3+import * as azimuth from 'azimuth-js';
4+import * as ob from 'urbit-ob';
5+
6+import {
7+ Button,
8+ Row,
9+ Col,
10+ Input,
11+ InnerLabel,
12+ ValidatedSigil,
13+ PointInput,
14+ TicketInput,
15+} from '../../components/old/Base';
16+
17+import * as need from '../../lib/need';
18+import { compose } from '../../lib/lib';
19+import { WALLET_TYPES, urbitWalletFromTicket } from '../../lib/wallet';
20+
21+import { withWallet } from '../../store/wallet';
22+import { withNetwork } from '../../store/network';
23+import { withPointCursor } from '../../store/pointCursor';
24+
25+//TODO should be part of InputWithStatus component
26+const INPUT_STATUS = {
27+ SPIN: Symbol('SPIN'),
28+ GOOD: Symbol('GOOD'),
29+ FAIL: Symbol('FAIL'),
30+};
31+
32+class Ticket extends React.Component {
33+ constructor(props) {
34+ super(props);
35+
36+ this.state = {
37+ pointName: '',
38+ ticket: '',
39+ ticketStatus: Nothing(),
40+ passphrase: '',
41+ };
42+
43+ this.handlePointNameInput = this.handlePointNameInput.bind(this);
44+ this.handleTicketInput = this.handleTicketInput.bind(this);
45+ this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
46+ this.verifyTicket = this.verifyTicket.bind(this);
47+ this.continue = this.continue.bind(this);
48+ }
49+
50+ componentDidMount() {
51+ //TODO deduce point name from URL if we can, prefill input if we found it
52+ }
53+
54+ handlePointNameInput(pointName) {
55+ if (pointName.length < 15) {
56+ this.setState({ pointName });
57+ this.verifyTicket(pointName, this.state.ticket, this.state.passphrase);
58+ }
59+ }
60+
61+ handleTicketInput(ticket) {
62+ this.setState({ ticket });
63+ this.verifyTicket(this.state.pointName, ticket, this.state.passphrase);
64+ }
65+
66+ handlePassphraseInput(passphrase) {
67+ this.setState({ passphrase });
68+ this.verifyTicket(this.state.pointName, this.state.ticket, passphrase);
69+ }
70+
71+ //TODO maybe want to do this only on-go, because wallet derivation is slow...
72+ async verifyTicket(pointName, ticket, passphrase) {
73+ if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
74+ this.props.setUrbitWallet(Nothing());
75+ this.setState({ ticketStatus: Nothing() });
76+ return;
77+ }
78+ this.setState({ ticketStatus: Just(INPUT_STATUS.SPIN) });
79+ const contracts = need.contracts(this.props);
80+ const pointNumber = ob.patp2dec(pointName);
81+ const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
82+ const isOwner = azimuth.azimuth.isOwner(
83+ contracts,
84+ pointNumber,
85+ uhdw.ownership.keys.address
86+ );
87+ const isTransferProxy = azimuth.azimuth.isTransferProxy(
88+ contracts,
89+ pointNumber,
90+ uhdw.ownership.keys.address
91+ );
92+ this.props.setUrbitWallet(Just(uhdw));
93+ this.setState({
94+ ticketStatus:
95+ (await isOwner) || (await isTransferProxy)
96+ ? Just(INPUT_STATUS.GOOD)
97+ : Just(INPUT_STATUS.FAIL),
98+ });
99+ }
100+
101+ canContinue() {
102+ // this is our only requirement, since we still want people with
103+ // non-standard wallet setups to be able to log in
104+ return Just.hasInstance(this.props.wallet);
105+ }
106+
107+ continue() {
108+ //TODO maybe do on-mount?
109+ this.props.setWalletType(WALLET_TYPES.TICKET);
110+ this.props.setPointCursor(Just(ob.patp2dec(this.state.pointName)));
111+ this.props.loginCompleted();
112+ }
113+
114+ render() {
115+ const pointInput = (
116+ <PointInput
117+ className="mono mt-8"
118+ prop-size="lg"
119+ prop-format="innerLabel"
120+ type="text"
121+ autoFocus
122+ value={this.state.pointName}
123+ onChange={this.handlePointNameInput}>
124+ <InnerLabel>{'Point name'}</InnerLabel>
125+ <ValidatedSigil
126+ className={'tr-0 mt-05 mr-0 abs'}
127+ patp={this.state.pointName}
128+ size={68}
129+ margin={8}
130+ />
131+ </PointInput>
132+ );
133+
134+ const ticketStatus = this.state.ticketStatus.matchWith({
135+ Nothing: () => <span />,
136+ Just: status => {
137+ switch (status.value) {
138+ case INPUT_STATUS.SPIN:
139+ return <span>⋯</span>;
140+ case INPUT_STATUS.GOOD:
141+ return <span>✓</span>;
142+ case INPUT_STATUS.FAIL:
143+ return <span>𐄂</span>;
144+ default:
145+ throw new Error('weird input status ' + status.value);
146+ }
147+ },
148+ });
149+
150+ const ticketInput = (
151+ //TODO NewTicketInput component, that does dashes etc
152+ <TicketInput
153+ className="mono mt-8"
154+ prop-size="lg"
155+ prop-format="innerLabel"
156+ type="password"
157+ name="ticket"
158+ value={this.state.ticket}
159+ onChange={this.handleTicketInput}>
160+ <InnerLabel>{'Master ticket'}</InnerLabel>
161+ {ticketStatus}
162+ </TicketInput>
163+ );
164+
165+ const passphraseInput = !this.props.advanced ? null : (
166+ <Input
167+ className="mt-8"
168+ prop-size="md"
169+ prop-format="innerLabel"
170+ name="passphrase"
171+ type="password"
172+ value={this.state.passphrase}
173+ autocomplete="off"
174+ onChange={this.handlePassphraseInput}>
175+ <InnerLabel>{'Passphrase'}</InnerLabel>
176+ </Input>
177+ );
178+
179+ return (
180+ <Row>
181+ <Col>
182+ {pointInput}
183+ {ticketInput}
184+ {passphraseInput}
185+
186+ <Button
187+ className={'mt-10'}
188+ disabled={!this.canContinue()}
189+ onClick={this.continue}>
190+ {'Go →'}
191+ </Button>
192+ </Col>
193+ </Row>
194+ );
195+ }
196+}
197+
198+export default compose(
199+ withWallet,
200+ withNetwork,
201+ withPointCursor
202+)(Ticket);
src/views/Login/Trezor.jsnew file+146
@@ -0,0 +1,146 @@
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 } from '../../components/old/Base';
6+import {
7+ InnerLabel,
8+ Input,
9+ InnerLabelDropdown,
10+} from '../../components/old/Base';
11+import TrezorConnect from 'trezor-connect';
12+import * as secp256k1 from 'secp256k1';
13+
14+import { TREZOR_PATH } from '../../lib/trezor';
15+import { compose } from '../../lib/lib';
16+import { withWallet } from '../../store/wallet';
17+
18+class Trezor extends React.Component {
19+ constructor(props) {
20+ super(props);
21+
22+ this.state = {
23+ hdpath: TREZOR_PATH.replace(/x/g, 0),
24+ account: 0,
25+ };
26+
27+ this.handleAccountSelection = this.handleAccountSelection.bind(this);
28+ this.handleHdPathInput = this.handleHdPathInput.bind(this);
29+ this.pollDevice = this.pollDevice.bind(this);
30+ }
31+
32+ handleAccountSelection(account) {
33+ let hdpath = this.state.hdpath;
34+ if (account !== 'custom') {
35+ hdpath = TREZOR_PATH.replace(/x/g, account);
36+ }
37+ this.setState({ account, hdpath });
38+ }
39+
40+ handleHdPathInput(hdpath) {
41+ this.setState({ hdpath });
42+ }
43+
44+ async pollDevice() {
45+ const { setWallet, setWalletHdPath } = this.props;
46+ const { hdpath } = this.state;
47+
48+ TrezorConnect.manifest({
49+ email: 'bridge-trezor@urbit.org',
50+ appUrl: 'https://github.com/urbit/bridge',
51+ });
52+ TrezorConnect.getPublicKey({ path: hdpath }).then(info => {
53+ if (info.success === true) {
54+ const payload = info.payload;
55+ const publicKey = Buffer.from(payload.publicKey, 'hex');
56+ const chainCode = Buffer.from(payload.chainCode, 'hex');
57+ const pub = secp256k1.publicKeyConvert(publicKey, true);
58+ const hd = bip32.fromPublicKey(pub, chainCode);
59+ setWallet(Maybe.Just(hd));
60+ setWalletHdPath(hdpath);
61+ } else {
62+ setWallet(Maybe.Nothing());
63+ }
64+ });
65+ }
66+
67+ render() {
68+ const { wallet } = this.props;
69+ const { hdpath, account } = this.state;
70+ const { handleAccountSelection } = this;
71+
72+ let accountOptions = [
73+ {
74+ title: 'Custom path',
75+ value: 'custom',
76+ },
77+ ];
78+ for (let i = 0; i < 20; i++) {
79+ accountOptions.push({
80+ title: 'Account #' + (i + 1),
81+ value: i,
82+ });
83+ }
84+ let accountTitle = accountOptions.find(o => o.value === account).title;
85+
86+ const accountSelection = (
87+ <InnerLabelDropdown
88+ className="mt-8"
89+ prop-size="md"
90+ prop-format="innerLabel"
91+ options={accountOptions}
92+ handleUpdate={handleAccountSelection}
93+ title={'Account'}
94+ currentSelectionTitle={accountTitle}
95+ fullWidth={true}
96+ />
97+ );
98+
99+ const pathSelection =
100+ account !== 'custom' ? null : (
101+ <Input
102+ className="mt-8 pt-8 text-mono"
103+ prop-size="md"
104+ prop-format="innerLabel"
105+ name="hdpath"
106+ value={hdpath}
107+ autocomplete="off"
108+ onChange={this.handleHdPathInput}>
109+ <InnerLabel>{'HD Path'}</InnerLabel>
110+ </Input>
111+ );
112+
113+ return (
114+ <Row>
115+ <Col className={'measure-md'}>
116+ <H1>{'Authenticate With Your Trezor'}</H1>
117+
118+ <P>
119+ {`Connect and authenticate to your Trezor. If you'd like
120+ to use a custom derivation path, you may enter it below.`}
121+ </P>
122+
123+ {accountSelection}
124+ {pathSelection}
125+
126+ <Button
127+ className={'mt-8'}
128+ prop-size={'wide lg'}
129+ onClick={this.pollDevice}>
130+ {'Authenticate →'}
131+ </Button>
132+
133+ <Button
134+ className={'mt-8'}
135+ prop-size={'wide lg'}
136+ disabled={Maybe.Nothing.hasInstance(wallet)}
137+ onClick={this.props.loginCompleted}>
138+ {'Continue →'}
139+ </Button>
140+ </Col>
141+ </Row>
142+ );
143+ }
144+}
145+
146+export default compose(withWallet)(Trezor);
src/views/PointHome.jsnew file+111
@@ -0,0 +1,111 @@
1+import { Just, Nothing } from 'folktale/maybe';
2+import React from 'react';
3+import * as azimuth from 'azimuth-js';
4+import * as need from '../lib/need';
5+
6+import { Row, Col, P } from '../components/old/Base';
7+
8+import { compose } from '../lib/lib';
9+
10+import { withNetwork } from '../store/network';
11+import { withPointCursor } from '../store/pointCursor';
12+import { withPointCache } from '../store/pointCache';
13+
14+class PointHome extends React.Component {
15+ constructor(props) {
16+ super(props);
17+
18+ this.state = {
19+ birthday: Nothing(),
20+ spawned: [],
21+ invites: Nothing(),
22+ };
23+ }
24+
25+ componentDidMount() {
26+ this.cachePoints();
27+ this.updateBirthday();
28+ }
29+
30+ //TODO can probably re-use this to implement point switching
31+ componentDidUpdate(prevProps) {
32+ //
33+ // NB (jtobin)
34+ //
35+ // When a link in the 'issued points' component is clicked, we don't
36+ // actually leave the Point component, and componentDidMount will not fire
37+ // again. But, we can force a call to cachePoints here.
38+ //
39+ const oldCursor = prevProps.pointCursor.getOrElse(true);
40+ const newCursor = this.props.pointCursor.getOrElse(true);
41+
42+ if (oldCursor !== newCursor) {
43+ this.cachePoints();
44+ }
45+ }
46+
47+ cachePoints() {
48+ const { web3, contracts, pointCursor, addToPointCache } = this.props;
49+
50+ web3.chain(_ =>
51+ contracts.chain(ctrcs =>
52+ pointCursor.chain(point => {
53+ azimuth.azimuth
54+ .getPoint(ctrcs, point)
55+ .then(details => addToPointCache({ [point]: details }));
56+ azimuth.delegatedSending
57+ .getTotalUsableInvites(ctrcs, point)
58+ .then(count => this.setState({ invites: Just(count) }));
59+ this.updateSpawned(ctrcs, point);
60+ })
61+ )
62+ );
63+ }
64+
65+ async updateBirthday() {
66+ const birthBlock = await azimuth.azimuth.getActivationBlock(
67+ need.contracts(this.props),
68+ need.pointCursor(this.props)
69+ );
70+ if (birthBlock === 0) {
71+ this.setState({ birthday: Nothing() });
72+ } else {
73+ const block = await need.web3(this.props).eth.getBlock(birthBlock);
74+ //TODO add to point cache
75+ this.setState({ birthday: Just(new Date(block.timestamp * 1000)) });
76+ }
77+ }
78+
79+ updateSpawned = contracts => {
80+ const { pointCursor } = this.props;
81+
82+ pointCursor.matchWith({
83+ Nothing: _ => {},
84+ Just: point => {
85+ azimuth.azimuth
86+ .getSpawned(contracts, point.value)
87+ .then(spawned => this.setState({ spawned }));
88+ },
89+ });
90+ };
91+
92+ render() {
93+ return (
94+ <Row>
95+ <Col>
96+ <P>Admin</P>
97+
98+ <P>Boot Arvo</P>
99+
100+ <P>Invite</P>
101+ </Col>
102+ </Row>
103+ );
104+ }
105+}
106+
107+export default compose(
108+ withNetwork,
109+ withPointCursor,
110+ withPointCache
111+)(PointHome);