on master help sign in
Merge pull request #171 from urbit/mino-login
0e91f6aFang
package-lock.json+3−3
@@ -2604,9 +2604,9 @@
2604 }
2604 }
2605 },
2605 },
2606 "azimuth-js": {
2606 "azimuth-js": {
2607− "version": "0.14.2",
2607+ "version": "0.15.0",
2608− "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.14.2.tgz",
2608+ "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.15.0.tgz",
2609− "integrity": "sha512-54CJ8K0dheso9U1sBWCHkMvUj7/EIEDGc/UH1HU2uenEkSQpx9Ic4jHq9H6W8baew//PEH4ldYnFFVfsd4Gi4Q==",
2609+ "integrity": "sha512-U/700d/94vvjDXpfXVPhrmYna3EX4aVRfkAdG21+k54LJTEozKAQySTPiuOpNsn9kEjaAp3sHR+C9glxm+5NEg==",
2610 "requires": {
2610 "requires": {
2611 "azimuth-solidity": "1.2.0",
2611 "azimuth-solidity": "1.2.0",
2612 "ethereumjs-util": "^5.2.0"
2612 "ethereumjs-util": "^5.2.0"
package.json+1−1
@@ -17,7 +17,7 @@
17 "@ledgerhq/hw-app-eth": "^4.61.0",
17 "@ledgerhq/hw-app-eth": "^4.61.0",
18 "@ledgerhq/hw-transport-u2f": "^4.61.0",
18 "@ledgerhq/hw-transport-u2f": "^4.61.0",
19 "PaperCollateralRenderer": "github:urbit/PaperCollateralRenderer",
19 "PaperCollateralRenderer": "github:urbit/PaperCollateralRenderer",
20− "azimuth-js": "^0.14.0",
20+ "azimuth-js": "^0.15.0",
21 "azimuth-solidity": "^1.2.0",
21 "azimuth-solidity": "^1.2.0",
22 "bip32": "^1.0.2",
22 "bip32": "^1.0.2",
23 "bip39": "^2.5.0",
23 "bip39": "^2.5.0",
src/bridge/components/Tabs.jsnew file+18
@@ -0,0 +1,18 @@
1+import React from 'react';
2+import HorizontalSelector from './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/bridge/lib/need.js+28−1
@@ -72,4 +72,31 @@ function fromPointCache(obj, point) {
72 })();
72 })();
73}
73}
74
74
75−export { web3, contracts, wallet, address, pointCursor, fromPointCache };
75+function keystore(obj) {
76+ return 'keystore' in obj === false
77+ ? (() => {
78+ throw BRIDGE_ERROR.MISSING_KEYSTORE;
79+ })()
80+ : obj.keystore.matchWith({
81+ Nothing: _ => {
82+ throw BRIDGE_ERROR.MISSING_KEYSTORE;
83+ },
84+ Just: ks =>
85+ ks.value.matchWith({
86+ Ok: result => result.value,
87+ Error: _ => {
88+ throw BRIDGE_ERROR.MISSING_KEYSTORE;
89+ },
90+ }),
91+ });
92+}
93+
94+export {
95+ web3,
96+ contracts,
97+ wallet,
98+ address,
99+ pointCursor,
100+ fromPointCache,
101+ keystore,
102+};
src/bridge/lib/routeNames.js+3
@@ -6,6 +6,9 @@ export const ROUTE_NAMES = {
6 INVITES_SEND: Symbol('INVITES_SEND'),
6 INVITES_SEND: Symbol('INVITES_SEND'),
7 INVITES_MANAGE: Symbol('INVITES_MANAGE'),
7 INVITES_MANAGE: Symbol('INVITES_MANAGE'),
8 //
8 //
9+ LOGIN: Symbol('LOGIN'),
10+ POINT_HOME: Symbol('POINT_HOME'),
11+ //
9 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
12 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
10 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
13 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
11 LANDING: Symbol('LANDING'),
14 LANDING: Symbol('LANDING'),
src/bridge/lib/router.js+8
@@ -6,6 +6,8 @@ import Landing from '../views/Landing';
6import InviteTicket from '../views/InviteTicket';
6import InviteTicket from '../views/InviteTicket';
7import InvitesSend from '../views/InvitesSend.js';
7import InvitesSend from '../views/InvitesSend.js';
8import InvitesManage from '../views/InvitesManage.js';
8import InvitesManage from '../views/InvitesManage.js';
9+import Login from '../views/Login.js';
10+import PointHome from '../views/PointHome.js';
9import AcceptTransfer from '../views/AcceptTransfer';
11import AcceptTransfer from '../views/AcceptTransfer';
10import CancelTransfer from '../views/CancelTransfer';
12import CancelTransfer from '../views/CancelTransfer';
11import CreateGalaxy from '../views/CreateGalaxy';
13import CreateGalaxy from '../views/CreateGalaxy';
@@ -42,6 +44,8 @@ const ROUTES = {
42 [ROUTE_NAMES.INVITE_TICKET]: InviteTicket,
44 [ROUTE_NAMES.INVITE_TICKET]: InviteTicket,
43 [ROUTE_NAMES.INVITES_SEND]: InvitesSend,
45 [ROUTE_NAMES.INVITES_SEND]: InvitesSend,
44 [ROUTE_NAMES.INVITES_MANAGE]: InvitesManage,
46 [ROUTE_NAMES.INVITES_MANAGE]: InvitesManage,
47+ [ROUTE_NAMES.LOGIN]: Login,
48+ [ROUTE_NAMES.POINT_HOME]: PointHome,
45 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
49 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
46 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
50 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
47 [ROUTE_NAMES.NETWORK]: Network,
51 [ROUTE_NAMES.NETWORK]: Network,
@@ -81,6 +85,10 @@ export const getRouteBreadcrumb = ({
81 return 'Send invites';
85 return 'Send invites';
82 case ROUTE_NAMES.INVITES_MANAGE:
86 case ROUTE_NAMES.INVITES_MANAGE:
83 return 'Manage invites';
87 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 }));
84 case ROUTE_NAMES.NETWORK:
92 case ROUTE_NAMES.NETWORK:
85 return renderNetworkType(networkType);
93 return renderNetworkType(networkType);
86 case ROUTE_NAMES.WALLET:
94 case ROUTE_NAMES.WALLET:
src/bridge/store/wallet.js+2
@@ -71,6 +71,8 @@ function _useWallet(
71 });
71 });
72
72
73 setWallet(wallet);
73 setWallet(wallet);
74+ } else {
75+ setWallet(Maybe.Nothing());
74 }
76 }
75
77
76 _setUrbitWallet(urbitWallet);
78 _setUrbitWallet(urbitWallet);
src/bridge/views/Landing.js+10
@@ -29,6 +29,16 @@ function Landing(props) {
29
29
30 <P>{'If you were sent an Azimuth invite code, start here.'}</P>
30 <P>{'If you were sent an Azimuth invite code, start here.'}</P>
31
31
32+ <Button
33+ prop-type="link"
34+ prop-size="lg"
35+ className={'mb-4 mt-8'}
36+ onClick={() => history.push(ROUTE_NAMES.LOGIN)}>
37+ {'Login (mino) →'}
38+ </Button>
39+
40+ <P>{'This landing page needs to be revamped too.'}</P>
41+
32 <Button
42 <Button
33 prop-type="link"
43 prop-type="link"
34 prop-size="lg"
44 prop-size="lg"
src/bridge/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/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+ onTabChance={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/bridge/views/Login/Advanced.jsnew file+115
@@ -0,0 +1,115 @@
1+import React from 'react';
2+import { P, InnerLabelDropdown } from '../../components/Base';
3+import { Row, Col } from '../../components/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/bridge/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/Base';
6+import { Input, InnerLabel, InputCaption } from '../../components/Base';
7+import { Row, Col, H1, H3, Warning } from '../../components/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/bridge/views/Login/Ledger.jsnew file+220
@@ -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);
src/bridge/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/Base';
4+import {
5+ Input,
6+ MnemonicInput,
7+ InnerLabel,
8+ InputCaption,
9+} from '../../components/Base';
10+import { Row, Col } from '../../components/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/bridge/views/Login/PrivateKey.jsnew file+76
@@ -0,0 +1,76 @@
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 { EthereumWallet } from '../../lib/wallet';
8+import { compose } from '../../lib/lib';
9+import { withWallet } from '../../store/wallet';
10+
11+class PrivateKey extends React.Component {
12+ constructor(props) {
13+ super(props);
14+
15+ this.state = {
16+ privateKey: '',
17+ };
18+
19+ this.handlePrivateKeyInput = this.handlePrivateKeyInput.bind(this);
20+ }
21+
22+ handlePrivateKeyInput(privateKey) {
23+ this.setState({ privateKey });
24+ this.constructWallet(privateKey);
25+ }
26+
27+ constructWallet(privateKey) {
28+ const { setWallet } = this.props;
29+ if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
30+ const sec = Buffer.from(privateKey, 'hex');
31+ const wallet = new EthereumWallet(sec);
32+ setWallet(Maybe.Just(wallet));
33+ } else {
34+ setWallet(Maybe.Nothing());
35+ }
36+ }
37+
38+ render() {
39+ const { wallet } = this.props;
40+ const { privateKey } = this.state;
41+
42+ return (
43+ <Row>
44+ <Col className={'measure-md'}>
45+ <H1 className={'mb-4'}>{'Enter Your Private Key'}</H1>
46+ <InputCaption>
47+ {`Please enter your raw Ethereum private key here.`}
48+ </InputCaption>
49+
50+ <RequiredInput
51+ className="pt-8 mt-8"
52+ prop-size="md"
53+ prop-format="innerLabel"
54+ type="password"
55+ name="privateKey"
56+ onChange={this.handlePrivateKeyInput}
57+ value={privateKey}
58+ autocomplete="off"
59+ autoFocus>
60+ <InnerLabel>{'Private Key'}</InnerLabel>
61+ </RequiredInput>
62+
63+ <Button
64+ className={'mt-10'}
65+ prop-size={'wide lg'}
66+ disabled={Maybe.Nothing.hasInstance(wallet)}
67+ onClick={this.props.loginCompleted}>
68+ {'Continue →'}
69+ </Button>
70+ </Col>
71+ </Row>
72+ );
73+ }
74+}
75+
76+export default compose(withWallet)(PrivateKey);
src/bridge/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/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 { 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/bridge/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/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/bridge/views/Login/Trezor.jsnew file+142
@@ -0,0 +1,142 @@
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 { compose } from '../../lib/lib';
12+import { withWallet } from '../../store/wallet';
13+
14+class Trezor extends React.Component {
15+ constructor(props) {
16+ super(props);
17+
18+ this.state = {
19+ hdpath: TREZOR_PATH.replace(/x/g, 0),
20+ account: 0,
21+ };
22+
23+ this.handleAccountSelection = this.handleAccountSelection.bind(this);
24+ this.handleHdPathInput = this.handleHdPathInput.bind(this);
25+ this.pollDevice = this.pollDevice.bind(this);
26+ }
27+
28+ handleAccountSelection(account) {
29+ let hdpath = this.state.hdpath;
30+ if (account !== 'custom') {
31+ hdpath = TREZOR_PATH.replace(/x/g, account);
32+ }
33+ this.setState({ account, hdpath });
34+ }
35+
36+ handleHdPathInput(hdpath) {
37+ this.setState({ hdpath });
38+ }
39+
40+ async pollDevice() {
41+ const { setWallet, setWalletHdPath } = this.props;
42+ const { hdpath } = this.state;
43+
44+ TrezorConnect.manifest({
45+ email: 'bridge-trezor@urbit.org',
46+ appUrl: 'https://github.com/urbit/bridge',
47+ });
48+ TrezorConnect.getPublicKey({ path: hdpath }).then(info => {
49+ if (info.success === true) {
50+ const payload = info.payload;
51+ const publicKey = Buffer.from(payload.publicKey, 'hex');
52+ const chainCode = Buffer.from(payload.chainCode, 'hex');
53+ const pub = secp256k1.publicKeyConvert(publicKey, true);
54+ const hd = bip32.fromPublicKey(pub, chainCode);
55+ setWallet(Maybe.Just(hd));
56+ setWalletHdPath(hdpath);
57+ } else {
58+ setWallet(Maybe.Nothing());
59+ }
60+ });
61+ }
62+
63+ render() {
64+ const { wallet } = this.props;
65+ const { hdpath, account } = this.state;
66+ const { handleAccountSelection } = this;
67+
68+ let accountOptions = [
69+ {
70+ title: 'Custom path',
71+ value: 'custom',
72+ },
73+ ];
74+ for (let i = 0; i < 20; i++) {
75+ accountOptions.push({
76+ title: 'Account #' + (i + 1),
77+ value: i,
78+ });
79+ }
80+ let accountTitle = accountOptions.find(o => o.value === account).title;
81+
82+ const accountSelection = (
83+ <InnerLabelDropdown
84+ className="mt-8"
85+ prop-size="md"
86+ prop-format="innerLabel"
87+ options={accountOptions}
88+ handleUpdate={handleAccountSelection}
89+ title={'Account'}
90+ currentSelectionTitle={accountTitle}
91+ fullWidth={true}
92+ />
93+ );
94+
95+ const pathSelection =
96+ account !== 'custom' ? null : (
97+ <Input
98+ className="mt-8 pt-8 text-mono"
99+ prop-size="md"
100+ prop-format="innerLabel"
101+ name="hdpath"
102+ value={hdpath}
103+ autocomplete="off"
104+ onChange={this.handleHdPathInput}>
105+ <InnerLabel>{'HD Path'}</InnerLabel>
106+ </Input>
107+ );
108+
109+ return (
110+ <Row>
111+ <Col className={'measure-md'}>
112+ <H1>{'Authenticate With Your Trezor'}</H1>
113+
114+ <P>
115+ {`Connect and authenticate to your Trezor. If you'd like
116+ to use a custom derivation path, you may enter it below.`}
117+ </P>
118+
119+ {accountSelection}
120+ {pathSelection}
121+
122+ <Button
123+ className={'mt-8'}
124+ prop-size={'wide lg'}
125+ onClick={this.pollDevice}>
126+ {'Authenticate →'}
127+ </Button>
128+
129+ <Button
130+ className={'mt-8'}
131+ prop-size={'wide lg'}
132+ disabled={Maybe.Nothing.hasInstance(wallet)}
133+ onClick={this.props.loginCompleted}>
134+ {'Continue →'}
135+ </Button>
136+ </Col>
137+ </Row>
138+ );
139+ }
140+}
141+
142+export default compose(withWallet)(Trezor);
src/bridge/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/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);