on master help sign in
Merge pull request #174 from urbit/mino-login-functional
df1910fFang
src/components/Inputs.js+29−5
@@ -2,11 +2,14 @@ import React from 'react';
2import { Input } from 'indigo-react';
2import { Input } from 'indigo-react';
3import * as bip39 from 'bip39';
3import * as bip39 from 'bip39';
4
4
5+import InputSigil from 'components/InputSigil';
6+
5import { DEFAULT_HD_PATH } from 'lib/wallet';
7import { DEFAULT_HD_PATH } from 'lib/wallet';
6import {
8import {
7 validateNotEmpty,
9 validateNotEmpty,
8 validateMnemonic,
10 validateMnemonic,
9 validatePoint,
11 validatePoint,
12+ validateTicket,
10} from 'lib/validators';
13} from 'lib/validators';
11import { prependSig } from 'lib/transformers';
14import { prependSig } from 'lib/transformers';
12
15
@@ -82,13 +85,34 @@ export function PointInput(props) {
82 validators={[validatePoint, validateNotEmpty]}
85 validators={[validatePoint, validateNotEmpty]}
83 transformers={[prependSig]}
86 transformers={[prependSig]}
84 mono
87 mono
88+ accessory={
89+ <InputSigil
90+ patp={props.initialValue || '~'}
91+ size={68}
92+ margin={8}
93+ pass={props.pass}
94+ onPass={props.setPass}
95+ focused={props.focused}
96+ onFocus={props.setFocused}
97+ error={props.error}
98+ onError={props.setError}
99+ />
100+ }
85 {...props}
101 {...props}
86 />
102 />
87 );
103 );
88}
104}
89
105
90−// const TicketInput = advancedInput({
106+//TODO needs to be fancier, displaying sig and dashes instead of •ing all
91−// WrappedComponent: Input,
107+export function TicketInput(props) {
108+ return (
109+ <Input
110+ type="password"
111+ placeholder="~master-ticket"
92−// validators: [validateTicket, validateNotEmpty],
112+ validators={[validateTicket, validateNotEmpty]}
93−// transformers: [prependSig],
113+ transformers={[prependSig]}
114+ mono
115+ {...props}
116+ />
94−// });
117+ );
118+}
src/views/Login.js+63−80
@@ -1,22 +1,23 @@
1import { Nothing, Just } from 'folktale/maybe';
1import { Nothing, Just } from 'folktale/maybe';
2−import React from 'react';
2+import React, { useState } from 'react';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4
4
5−import { H1, Row, Col } from '../components/old/Base';
5+import { H1 } from 'indigo-react';
6+import View from 'components/View';
6−import Tabs from '../components/Tabs';
7+import Tabs from 'components/Tabs';
7
8
8import Ticket from './Login/Ticket';
9import Ticket from './Login/Ticket';
9import Mnemonic from './Login/Mnemonic';
10import Mnemonic from './Login/Mnemonic';
10import Advanced from './Login/Advanced';
11import Advanced from './Login/Advanced';
11
12
13+import useLifecycle from 'lib/useLifecycle';
12−import * as need from '../lib/need';
14+import * as need from 'lib/need';
13−import { compose } from '../lib/lib';
14−import { ROUTE_NAMES } from '../lib/routeNames';
15+import { ROUTE_NAMES } from 'lib/routeNames';
15
16
16−import { withHistory } from '../store/history';
17+import { useHistory } from 'store/history';
17−import { withNetwork } from '../store/network';
18+import { useNetwork } from 'store/network';
18−import { withWallet } from '../store/wallet';
19+import { useWallet } from 'store/wallet';
19−import { withPointCursor } from '../store/pointCursor';
20+import { usePointCursor } from 'store/pointCursor';
20
21
21const TABS = {
22const TABS = {
22 TICKET: Symbol('TICKET'),
23 TICKET: Symbol('TICKET'),
@@ -24,46 +25,51 @@ const TABS = {
24 ADVANCED: Symbol('ADVANCED'),
25 ADVANCED: Symbol('ADVANCED'),
25};
26};
26
27
27−class Login extends React.Component {
28+const tabViews = {
28− constructor(props) {
29+ [TABS.TICKET]: Ticket,
29− super(props);
30+ [TABS.MNEMONIC]: Mnemonic,
31+ [TABS.ADVANCED]: Advanced,
32+};
30
33
31− this.state = {
34+const tabOptions = [
32− currentTab: TABS.TICKET,
35+ { title: 'Ticket', value: TABS.TICKET },
36+ { title: 'Mnemonic', value: TABS.MNEMONIC },
37+ { title: 'Advanced', value: TABS.ADVANCED },
33− };
38+];
34
39
35− this.handleTabChange = this.handleTabChange.bind(this);
40+export default function Login() {
36− this.continue = this.continue.bind(this);
41+ // globals
37− }
42+ const history = useHistory();
43+ const { contracts } = useNetwork();
44+ const { wallet, setUrbitWallet } = useWallet();
45+ const { pointCursor, setPointCursor } = usePointCursor();
38
46
39− componentDidMount() {
47+ // inputs
40− // we expect wallet and pointCursor to not be set yet
48+ const [currentTab, setCurrentTab] = useState(TABS.TICKET);
41− this.props.setUrbitWallet(Nothing());
42− this.props.setWallet(Nothing());
43− this.props.setPointCursor(Nothing());
44− }
45
49
46− handleTabChange(currentTab) {
50+ // on-mount
47− this.setState({ currentTab });
51+ useLifecycle(() => {
52+ setUrbitWallet(Nothing());
53+ setPointCursor(Nothing());
48− }
54+ });
49
55
50− async continue() {
56+ const doContinue = async () => {
51− const wallet = need.wallet(this.props);
57+ const _wallet = need.wallet(wallet);
52− const contracts = need.contracts(this.props);
58+ const _contracts = need.contracts(contracts);
53
59
54 // if no point cursor set by login logic, try to deduce it
60 // if no point cursor set by login logic, try to deduce it
55 let deduced = Nothing();
61 let deduced = Nothing();
56− if (Nothing.hasInstance(this.props.pointCursor)) {
62+ if (Nothing.hasInstance(pointCursor)) {
57 const owned = await azimuth.azimuth.getOwnedPoints(
63 const owned = await azimuth.azimuth.getOwnedPoints(
58− contracts,
64+ _contracts,
59− wallet.address
65+ _wallet.address
60 );
66 );
61 if (owned.length === 1) {
67 if (owned.length === 1) {
62 deduced = Just(owned[0]);
68 deduced = Just(owned[0]);
63 } else if (owned.length === 0) {
69 } else if (owned.length === 0) {
64 const canOwn = await azimuth.azimuth.getTransferringFor(
70 const canOwn = await azimuth.azimuth.getTransferringFor(
65− contracts,
71+ _contracts,
66− wallet.address
72+ _wallet.address
67 );
73 );
68 if (canOwn.length === 1) {
74 if (canOwn.length === 1) {
69 deduced = Just(canOwn[0]);
75 deduced = Just(canOwn[0]);
@@ -74,50 +80,27 @@ class Login extends React.Component {
74 // if we have a deduced point or one in the global context,
80 // if we have a deduced point or one in the global context,
75 // navigate to that specific point, otherwise navigate to list of points
81 // navigate to that specific point, otherwise navigate to list of points
76 if (Just.hasInstance(deduced)) {
82 if (Just.hasInstance(deduced)) {
77− this.props.setPointCursor(deduced);
83+ setPointCursor(deduced);
78− this.props.history.popAndPush(ROUTE_NAMES.POINT);
84+ history.popAndPush(ROUTE_NAMES.POINT);
79− } else if (Just.hasInstance(this.props.pointCursor)) {
85+ } else if (Just.hasInstance(pointCursor)) {
80− this.props.history.popAndPush(ROUTE_NAMES.POINT);
86+ history.popAndPush(ROUTE_NAMES.POINT);
81 } else {
87 } else {
82− this.props.history.popAndPush(ROUTE_NAMES.POINTS);
88+ history.popAndPush(ROUTE_NAMES.POINTS);
83 }
89 }
84− }
90+ };
85−
91+
86− render() {
87− const tabOptions = [
88− { title: 'Ticket', value: TABS.TICKET },
89− { title: 'Mnemonic', value: TABS.MNEMONIC },
90− { title: 'Advanced', value: TABS.ADVANCED },
91− ];
92−
93− const tabViews = {
94− [TABS.TICKET]: Ticket,
95− [TABS.MNEMONIC]: Mnemonic,
96− [TABS.ADVANCED]: Advanced,
97− };
98−
99− return (
92+ return (
100− <Row>
93+ <View>
101− <Col>
102− <H1>{'Login'}</H1>
94+ <H1>Login</H1>
103−
95+
104− <Tabs
96+ <Tabs
105− tabViews={tabViews}
97+ tabViews={tabViews}
106− tabOptions={tabOptions}
98+ tabOptions={tabOptions}
107− currentTab={this.state.currentTab}
99+ currentTab={currentTab}
108− onTabChange={this.handleTabChange}
100+ onTabChange={setCurrentTab}
109− //
101+ //
110− loginCompleted={this.continue}
102+ loginCompleted={doContinue}
111− />
103+ />
112− </Col>
104+ </View>
113− </Row>
114− );
105+ );
115− }
116}
106}
117−
118−export default compose(
119− withHistory,
120− withNetwork,
121− withWallet,
122− withPointCursor
123−)(Login);
src/views/Login/Keystore.js+87−116
@@ -1,66 +1,56 @@
1import { Just, Nothing } from 'folktale/maybe';
1import { Just, Nothing } from 'folktale/maybe';
2import { Ok, Error } from 'folktale/result';
2import { Ok, Error } from 'folktale/result';
3−import React from 'react';
3+import React, { useState } from 'react';
4import * as keythereum from 'keythereum';
4import * 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
5
24− this.handleKeystoreUpload = this.handleKeystoreUpload.bind(this);
6+import View from 'components/View';
25− this.handlePasswordInput = this.handlePasswordInput.bind(this);
7+import { PassphraseInput } from 'components/Inputs';
26− this.constructWallet = this.constructWallet.bind(this);
8+import { ForwardButton } from 'components/Buttons';
27− }
9+import { H3 } from 'indigo-react';
10+import { InputCaption, UploadButton, Warning } from 'components/old/Base';
11+
12+import * as need from 'lib/need';
13+import { EthereumWallet } from 'lib/wallet';
14+import { useWallet } from 'store/wallet';
28
15
29− handlePasswordInput(password) {
16+export default function Keystore({ loginCompleted }) {
30− this.setState({ password });
17+ // globals
31− }
18+ const { wallet, setWallet } = useWallet();
32
19
33− constructWallet() {
20+ // inputs
34− const { setWallet } = this.props;
21+ // keystore: Maybe<Result<String, String>>
22+ const [keystore, setKeystore] = useState(Nothing());
35− const { password } = this.state;
23+ const [password, setPassword] = useState('');
24+ const [decryptionProblem, setDecryptionProblem] = useState(false);
36
25
26+ const constructWallet = () => {
37 try {
27 try {
38− const text = need.keystore(this.state);
28+ const text = need.keystore(keystore);
39
29
40 const json = JSON.parse(text);
30 const json = JSON.parse(text);
41 const privateKey = keythereum.recover(password, json);
31 const privateKey = keythereum.recover(password, json);
42
32
43− const wallet = new EthereumWallet(privateKey);
33+ const newWallet = new EthereumWallet(privateKey);
44− this.setState({ decryptionProblem: false });
34+ setDecryptionProblem(false);
45− setWallet(Just(wallet));
35+ setWallet(Just(newWallet));
46 } catch (err) {
36 } catch (err) {
47− this.setState({ decryptionProblem: true });
37+ setDecryptionProblem(true);
48 setWallet(Nothing());
38 setWallet(Nothing());
49 }
39 }
50− }
40+ };
51
41
52− handleKeystoreUpload = event => {
42+ const handleKeystoreUpload = event => {
53 const file = event.files.item(0);
43 const file = event.files.item(0);
54 const reader = new FileReader();
44 const reader = new FileReader();
55
45
56 reader.onload = e => {
46 reader.onload = e => {
57 const keystore = e.target.result;
47 const keystore = e.target.result;
58− this.setState({ keystore: Just(Ok(keystore)) });
48+ setKeystore(Just(Ok(keystore)));
59 };
49 };
60
50
61 const failure = _ => {
51 const failure = _ => {
62 const message = 'There was a problem uploading your Keystore file';
52 const message = 'There was a problem uploading your Keystore file';
63− this.setState({ keystore: Just(Error(message)) });
53+ setKeystore(Just(Error(message)));
64 };
54 };
65
55
66 reader.onerror = failure;
56 reader.onerror = failure;
@@ -69,83 +59,64 @@ class Keystore extends React.Component {
69 reader.readAsText(file);
59 reader.readAsText(file);
70 };
60 };
71
61
72− render() {
73− const { wallet } = this.props;
74− const { keystore, password, decryptionProblem } = this.state;
75−
76− const uploadButtonClass = keystore.matchWith({
62+ const uploadButtonClass = keystore.matchWith({
77− Nothing: _ => 'bg-blue white',
63+ Nothing: _ => 'bg-blue white',
78− Just: ks =>
64+ Just: ks =>
79− ks.value.matchWith({
65+ ks.value.matchWith({
80− Ok: _ => 'bg-green white',
66+ Ok: _ => 'bg-green white',
81− Error: _ => 'bg-yellow black',
67+ Error: _ => 'bg-yellow black',
82− }),
68+ }),
83− });
69+ });
84−
70+
85− const decryptMessage =
71+ const decryptMessage =
86− decryptionProblem === false ? (
72+ decryptionProblem === false ? (
87− <div />
73+ <div />
88− ) : (
74+ ) : (
89− <Warning className={'mt-8'}>
75+ <Warning className="mt-8">
90− <H3 style={{ marginTop: 0, paddingTop: 0 }}>
76+ <H3 style={{ marginTop: 0, paddingTop: 0 }}>
91− {"Couldn't decrypt wallet."}
77+ Couldn't decrypt wallet.
92− </H3>
78+ </H3>
93− {'You may have entered an incorrect password.'}
79+ You may have entered an incorrect password.
94− </Warning>
80+ </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 );
81 );
148− }
149−}
150
82
151−export default compose(withWallet)(Keystore);
83+ return (
84+ <View>
85+ <InputCaption>
86+ Please upload your Ethereum keystore file. If your keystore file is
87+ encrypted with a password, you'll also need to enter that below.
88+ </InputCaption>
89+
90+ <UploadButton
91+ className={`${uploadButtonClass} mt3`}
92+ onChange={handleKeystoreUpload}>
93+ <div className="flex-center-all fs-4 h-11 pointer">
94+ Upload Keystore file
95+ </div>
96+ </UploadButton>
97+
98+ <PassphraseInput
99+ name="password"
100+ label="Keystore password"
101+ initialValue={password}
102+ onValue={setPassword}
103+ autoFocus></PassphraseInput>
104+
105+ <ForwardButton
106+ className="mt3"
107+ disabled={Nothing.hasInstance(keystore)}
108+ onClick={constructWallet}>
109+ Decrypt
110+ </ForwardButton>
111+
112+ {decryptMessage}
113+
114+ <ForwardButton
115+ className="mt3"
116+ disabled={Nothing.hasInstance(wallet)}
117+ onClick={loginCompleted}>
118+ Continue
119+ </ForwardButton>
120+ </View>
121+ );
122+}
src/views/Login/Ledger.js+142−186
@@ -1,69 +1,56 @@
1import * as bip32 from 'bip32';
1import * as bip32 from 'bip32';
2−import React from 'react';
2+import React, { useState } from 'react';
3+import { times } from 'lodash';
3import Maybe from 'folktale/maybe';
4import 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';
11import Transport from '@ledgerhq/hw-transport-u2f';
5import Transport from '@ledgerhq/hw-transport-u2f';
12import Eth from '@ledgerhq/hw-app-eth';
6import Eth from '@ledgerhq/hw-app-eth';
13import * as secp256k1 from 'secp256k1';
7import * as secp256k1 from 'secp256k1';
14
8
15−import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from '../../lib/ledger';
9+import View from 'components/View';
16−import { compose } from '../../lib/lib';
10+import { ForwardButton } from 'components/Buttons';
11+import { H1, H2, P, Input } from 'indigo-react';
12+import { InnerLabel, InnerLabelDropdown } from 'components/old/Base';
13+
14+import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from 'lib/ledger';
17−import { withWallet } from '../../store/wallet';
15+import { useWallet } from 'store/wallet';
16+
17+const pathOptions = [
18+ { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
19+ { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
20+ { title: 'Custom path', value: 'custom' },
21+];
18
22
19const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
23const chopHdPrefix = str => (str.slice(0, 2) === 'm/' ? str.slice(2) : str);
20
24
21const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
25const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
22
26
23−class Ledger extends React.Component {
27+export default function Ledger({ loginCompleted }) {
24− constructor(props) {
28+ const { wallet, setWallet, setWalletHdPath } = useWallet();
25− super(props);
29+
26−
30+ const [basePath, setBasePath] = useState(LEDGER_LIVE_PATH);
27− this.state = {
31+ const [account, setAccount] = useState(0);
28− basePath: LEDGER_LIVE_PATH,
29− hdpath: LEDGER_LIVE_PATH.replace(/x/g, 0),
32+ const [hdPath, setHdPath] = useState(LEDGER_LIVE_PATH.replace(/x/g, 0));
30− account: 0,
33+
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);
34+ const updateHdPath = (basePath, 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') {
35 if (basePath !== 'custom') {
51− hdpath = basePath.replace(/x/g, account);
36+ setHdPath(basePath.replace(/x/g, account));
52 }
37 }
53− this.setState({ basePath, account, hdpath });
38+ };
54− }
55
39
56− handleHdPathInput(hdpath) {
40+ const handlePathSelection = basePath => {
57− this.setState({ hdpath });
41+ setBasePath(basePath);
42+ updateHdPath(basePath, account);
58− }
43+ };
59
44
60− async pollDevice() {
45+ const handleAccountSelection = account => {
61− const { setWallet, setWalletHdPath } = this.props;
46+ setAccount(account);
62− const { hdpath } = this.state;
47+ updateHdPath(basePath, account);
48+ };
63
49
50+ const pollDevice = async () => {
64 const transport = await Transport.create();
51 const transport = await Transport.create();
65 const eth = new Eth(transport);
52 const eth = new Eth(transport);
66− const path = chopHdPrefix(hdpath);
53+ const path = chopHdPrefix(hdPath);
67
54
68 eth.getAddress(path, false, true).then(
55 eth.getAddress(path, false, true).then(
69 info => {
56 info => {
@@ -72,153 +59,122 @@ class Ledger extends React.Component {
72 const pub = secp256k1.publicKeyConvert(publicKey, true);
59 const pub = secp256k1.publicKeyConvert(publicKey, true);
73 const hd = bip32.fromPublicKey(pub, chainCode);
60 const hd = bip32.fromPublicKey(pub, chainCode);
74 setWallet(Maybe.Just(hd));
61 setWallet(Maybe.Just(hd));
75− setWalletHdPath(addHdPrefix(hdpath));
62+ setWalletHdPath(addHdPrefix(hdPath));
76 },
63 },
77 _ => {
64 _ => {
78 setWallet(Maybe.Nothing());
65 setWallet(Maybe.Nothing());
79 }
66 }
80 );
67 );
81− }
68+ };
82−
69+
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;
70+ const basePathTitle = pathOptions.find(o => o.value === basePath).title;
97−
71+
98− let accountOptions = [];
72+ const accountOptions = times(20, i => ({
99− for (let i = 0; i < 20; i++) {
100− accountOptions.push({
101− title: 'Account #' + (i + 1),
73+ title: `Account #${i + 1}`,
102− value: i,
74+ value: i,
103− });
75+ }));
104− }
105− let accountTitle = accountOptions.find(o => o.value === account).title;
76+ const accountTitle = accountOptions.find(o => o.value === account).title;
106−
77+
107− const basePathSelection = (
78+ const basePathSelection = (
79+ <InnerLabelDropdown
80+ className="mt-8"
81+ options={pathOptions}
82+ handleUpdate={handlePathSelection}
83+ title="Derivation path"
84+ currentSelectionTitle={basePathTitle}
85+ fullWidth={true}
86+ />
87+ );
88+
89+ const truePathSelection =
90+ basePath === 'custom' ? (
91+ <Input
92+ className="mt3"
93+ name="hdPath"
94+ label="HD path"
95+ autocomplete="off"
96+ initialValue={addHdPrefix(hdPath)}
97+ onValue={setHdPath}
98+ />
99+ ) : (
108 <InnerLabelDropdown
100 <InnerLabelDropdown
109− className="mt-8"
101+ className="mt-4"
110− options={pathOptions}
102+ prop-size="md"
111− handleUpdate={handlePathSelection}
103+ prop-format="innerLabel"
112− title={'Derivation path'}
104+ options={accountOptions}
105+ handleUpdate={handleAccountSelection}
106+ title="Account"
113− currentSelectionTitle={basePathTitle}
107+ currentSelectionTitle={accountTitle}
114 fullWidth={true}
108 fullWidth={true}
115 />
109 />
116 );
110 );
117
111
118− const truePathSelection =
112+ // when not on https, tell user how to get there
119− basePath === 'custom' ? (
113+ const body =
120− <Input
114+ document.location.protocol !== 'https:' ? (
121− className="pt-8 mt-4 text-mono"
115+ <>
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>
116+ <H2>Running on HTTP?</H2>
164−
117+
118+ <P>
165− <P>{`To authenticate and sign transactions with a Ledger, Bridge
119+ To authenticate and sign transactions with a Ledger, Bridge must be
166− must be serving over HTTPS on localhost. You can do this via the
120+ serving over HTTPS on localhost. You can do this via the following:
167− following:`}</P>
121+ </P>
168−
122+
169− <ol className={'measure-md'}>
123+ <ol className="measure-md">
170− <li className={'mt-4'}>
124+ <li className="mt-4">
171− {'Install'}{' '}
125+ Install
172− <a
173− target={'_blank'}
174− href={'https://github.com/FiloSottile/mkcert'}>
126+ <a target="_blank" href="https://github.com/FiloSottile/mkcert">
175− {'mkcert'}
127+ mkcert
176− </a>
128+ </a>
177− </li>
129+ </li>
178− <li className={'mt-4'}>
130+ <li className="mt-4">
179− {'Install a local certificate authority via '}
131+ Install a local certificate authority via{' '}
180− <code>{'mkcert -install'}</code>
132+ <code>mkcert -install</code>
181− </li>
133+ </li>
182− <li className={'mt-4'}>
134+ <li className="mt-4">
183− {'In your '}
184− <code>{'bridge'}</code>
185− {
186− ' directory, generate a certificate valid for localhost via '
135+ In your <code>bridge</code> directory, generate a certificate valid
187− }{' '}
188− <code>{'mkcert localhost'}</code>
136+ for localhost via <code>mkcert localhost</code>. This will produce
189− {'.'}
190− {'This will produce two files: '}
191− <code>{'localhost.pem'}</code> {', the local certificate, and '}
137+ two files: <code>localhost.pem</code>, the local certificate, and
192− <code>{'localhost-key.pem'}</code>
193− {', its corresponding private key.'}
138+ <code>localhost-key.pem</code> , its corresponding private key.
194− </li>
139+ </li>
195− <li className={'mt-4'}>
140+ <li className="mt-4">
196− {'Run '}
197− <code>{'python bridge-https.py'}</code>
141+ Run <code>python bridge-https.py</code>
198− </li>
142+ </li>
199− </ol>
143+ </ol>
200−
144+ </>
145+ ) : (
146+ <>
147+ <P>
148+ Connect and authenticate to your Ledger, and then open the "Ethereum"
149+ application. If you're running on older firmware, make sure the
150+ "browser support" option is turned on. To sign transactions, you'll
151+ also need to enable the "contract data" option.
152+ </P>
153+
154+ <P>
155+ If you'd like to use a custom derivation path, you may enter it below.
156+ </P>
157+
201− {basePathSelection}
158+ {basePathSelection}
202− {truePathSelection}
159+ {truePathSelection}
203−
160+
204− <Button
205− prop-size={'wide lg'}
206− className={'mt-8'}
207− onClick={this.pollDevice}>
161+ <ForwardButton className={'mt3'} onClick={pollDevice}>
208− {'Authenticate →'}
162+ {'Authenticate'}
209− </Button>
163+ </ForwardButton>
210−
164+
211− <Button
165+ <ForwardButton
212− className={'mt-8'}
166+ className={'mt3'}
213− prop-size={'wide lg'}
214− disabled={Maybe.Nothing.hasInstance(wallet)}
167+ disabled={Maybe.Nothing.hasInstance(wallet)}
215− onClick={this.props.loginCompleted}>
168+ onClick={loginCompleted}>
216− {'Continue →'}
169+ {'Continue'}
217− </Button>
170+ </ForwardButton>
218− </Col>
171+ </>
219− </Row>
220 );
172 );
221− }
222−}
223
173
224−export default compose(withWallet)(Ledger);
174+ return (
175+ <View>
176+ <H1>Authenticate With Your Ledger</H1>
177+ {body}
178+ </View>
179+ );
180+}
src/views/Login/Mnemonic.js+74−157
@@ -1,160 +1,77 @@
1+import React, { useCallback, useEffect, useState } from 'react';
1import { Just, Nothing } from 'folktale/maybe';
2import { 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
3
12−import { DEFAULT_HD_PATH, walletFromMnemonic } from '../../lib/wallet';
4+import View from 'components/View';
5+import { MnemonicInput, PassphraseInput, HdPathInput } from 'components/Inputs';
6+import { ForwardButton } from 'components/Buttons';
7+
13−import { compose } from '../../lib/lib';
8+import { walletFromMnemonic } from 'lib/wallet';
14−import { withWallet } from '../../store/wallet';
9+import { useWallet } from 'store/wallet';
15−
10+
16−class Mnemonic extends React.Component {
11+export default function Mnemonic({ advanced, loginCompleted }) {
17− constructor(props) {
12+ const {
18− super(props);
13+ wallet,
19−
14+ setWallet,
20− this.state = {
15+ authMnemonic,
21− mnemonic: '',
16+ setAuthMnemonic,
22− passphrase: '',
17+ walletHdPath,
23− hdpath: DEFAULT_HD_PATH,
18+ setWalletHdPath,
24− };
19+ } = useWallet();
25−
20+ const [passphrase, setPassphrase] = useState('');
26− this.handleMnemonicInput = this.handleMnemonicInput.bind(this);
21+ const mnemonic = authMnemonic.getOrElse('');
27− this.handlePassphraseInput = this.handlePassphraseInput.bind(this);
22+
28− this.handleHdPathInput = this.handleHdPathInput.bind(this);
23+ // TODO: move this into transformers?
29− }
24+ // transform the result of the mnemonic to Maybe<string>
30−
25+ const _setAuthMnemonic = useCallback(
31− componentDidMount() {
26+ mnemonic => setAuthMnemonic(mnemonic === '' ? Nothing() : Just(mnemonic)),
32− const { mnemonic, hdpath, passphrase } = this.state;
27+ [setAuthMnemonic]
33− this.attemptWalletDerivation(mnemonic, hdpath, passphrase);
28+ );
34− }
29+
35−
30+ // when the properties change, re-derive wallet
36− handlePassphraseInput(passphrase) {
31+ useEffect(() => {
37− this.setState((state, _) => {
32+ let mounted = true;
38− const mnemonic = state.mnemonic;
33+ (async () => {
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;
34+ const wallet = walletFromMnemonic(mnemonic, walletHdPath, passphrase);
92−
35+ mounted && setWallet(wallet);
36+ })();
37+ return () => (mounted = false);
38+ }, [mnemonic, passphrase, walletHdPath, setWallet]);
39+
93− return (
40+ return (
94− <Row>
41+ <View>
95− <Col>
42+ <MnemonicInput
96− <InputCaption>
43+ name="mnemonic"
97− {'Please enter your BIP39 mnemonic here.'}
44+ label="BIP39 Mnemonic"
98− </InputCaption>
99−
100− <MnemonicInput
101− className="pt-8"
102− prop-size="md"
103− prop-format="innerLabel"
104− type="text"
105− name="mnemonic"
45+ initialValue={mnemonic}
106− onChange={this.handleMnemonicInput}
46+ onValue={_setAuthMnemonic}
107− value={mnemonic}
108− autocomplete="off"
109− autoFocus>
47+ autoFocus
110− <InnerLabel>{'Mnemonic'}</InnerLabel>
48+ />
111− </MnemonicInput>
49+
112−
113− {!this.props.advanced ? null : (
50+ {!advanced ? null : (
114− <>
51+ <>
115− <InputCaption>
52+ <PassphraseInput
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"
53+ name="passphrase"
124− type="password"
54+ label="(Optional) Wallet Passphrase"
125− value={passphrase}
55+ initialValue={passphrase}
126− autocomplete="off"
56+ onValue={setPassphrase}
127− onChange={this.handlePassphraseInput}>
57+ />
128− <InnerLabel>{'Passphrase'}</InnerLabel>
58+
129− </Input>
59+ <HdPathInput
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"
60+ name="hdpath"
140− value={hdpath}
61+ label="HD Path"
141− autocomplete="off"
62+ initialValue={walletHdPath}
142− onChange={this.handleHdPathInput}>
63+ onValue={setWalletHdPath}
143− <InnerLabel>{'HD Path'}</InnerLabel>
64+ />
144− </Input>
145− </>
65+ </>
146− )}
66+ )}
147−
67+
148− <Button
68+ <ForwardButton
149− className={'mt-10'}
69+ className="mt3"
150− disabled={Nothing.hasInstance(wallet)}
70+ disabled={Nothing.hasInstance(wallet)}
151− onClick={this.props.loginCompleted}>
71+ onClick={loginCompleted}
152− {'Go →'}
72+ solid>
153− </Button>
73+ Continue
154− </Col>
74+ </ForwardButton>
155− </Row>
75+ </View>
156− );
76+ );
157− }
158}
77}
159−
160−export default compose(withWallet)(Mnemonic);
src/views/Login/PrivateKey.js+40−69
@@ -1,80 +1,51 @@
1−import Maybe from 'folktale/maybe';
1+import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
2+import React, { useState } 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
3
11−import { EthereumWallet } from '../../lib/wallet';
4+import View from 'components/View';
12−import { compose } from '../../lib/lib';
5+import { PassphraseInput } from 'components/Inputs';
13−import { withWallet } from '../../store/wallet';
6+import { ForwardButton } from 'components/Buttons';
14
7
15−class PrivateKey extends React.Component {
8+import { EthereumWallet } from 'lib/wallet';
16− constructor(props) {
9+import { useWallet } from 'store/wallet';
17− super(props);
18
10
19− this.state = {
11+export default function PrivateKey({ loginCompleted }) {
20− privateKey: '',
12+ const { wallet, setWallet } = useWallet();
21− };
22
13
23− this.handlePrivateKeyInput = this.handlePrivateKeyInput.bind(this);
14+ const [privateKey, setPrivateKey] = useState('');
24− }
25
15
26− handlePrivateKeyInput(privateKey) {
16+ const handlePrivateKeyInput = privateKey => {
27− this.setState({ privateKey });
17+ setPrivateKey(privateKey);
28− this.constructWallet(privateKey);
18+ constructWallet(privateKey);
29− }
19+ };
30
20
31− constructWallet(privateKey) {
21+ const constructWallet = privateKey => {
32− const { setWallet } = this.props;
33 if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
22 if (/^[0-9A-Fa-f]{64}$/g.test(privateKey) === true) {
34 const sec = Buffer.from(privateKey, 'hex');
23 const sec = Buffer.from(privateKey, 'hex');
35− const wallet = new EthereumWallet(sec);
24+ const newWallet = new EthereumWallet(sec);
36− setWallet(Maybe.Just(wallet));
25+ setWallet(Just(newWallet));
37 } else {
26 } else {
38− setWallet(Maybe.Nothing());
27+ setWallet(Nothing());
39 }
28 }
40− }
29+ };
41−
30+
42− render() {
43− const { wallet } = this.props;
44− const { privateKey } = this.state;
45−
46− return (
31+ return (
47− <Row>
32+ <View>
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.`}
33+ Please enter your raw Ethereum private key here.
52− </InputCaption>
34+ <PassphraseInput
53−
54− <RequiredInput
55− className="pt-8 mt-8"
56− prop-size="md"
57− prop-format="innerLabel"
58− type="password"
35+ type="password"
59− name="privateKey"
36+ name="privateKey"
37+ label="Private key"
38+ initialValue={privateKey}
60− onChange={this.handlePrivateKeyInput}
39+ onValue={handlePrivateKeyInput}
61− value={privateKey}
62− autocomplete="off"
40+ autocomplete="off"
63− autoFocus>
41+ autoFocus
64− <InnerLabel>{'Private Key'}</InnerLabel>
42+ />
65− </RequiredInput>
43+ <ForwardButton
66−
67− <Button
68− className={'mt-10'}
44+ className="mt3"
69− prop-size={'wide lg'}
70− disabled={Maybe.Nothing.hasInstance(wallet)}
45+ disabled={Nothing.hasInstance(wallet)}
71− onClick={this.props.loginCompleted}>
46+ onClick={loginCompleted}>
72− {'Continue →'}
47+ Continue
73− </Button>
48+ </ForwardButton>
74− </Col>
49+ </View>
75− </Row>
76− );
50+ );
77− }
78}
51}
79−
80−export default compose(withWallet)(PrivateKey);
src/views/Login/Shards.js+67−192
@@ -1,78 +1,24 @@
1import { Just, Nothing } from 'folktale/maybe';
1import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
2+import React, { useState } 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';
13import * as kg from 'urbit-key-generation/dist/index';
3import * as kg from 'urbit-key-generation/dist/index';
14−import * as ob from 'urbit-ob';
15
4
16−import { urbitWalletFromTicket } from '../../lib/wallet';
5+import View from 'components/View';
17−import { compose } from '../../lib/lib';
6+import { PointInput, TicketInput, PassphraseInput } from 'components/Inputs';
18−import { withWallet } from '../../store/wallet';
7+import { ForwardButton } from 'components/Buttons';
19
8
20−const placeholder = len => {
9+import { urbitWalletFromTicket } from 'lib/wallet';
21− let bytes = window.crypto.getRandomValues(new Uint8Array(len));
10+import { useWallet } from 'store/wallet';
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
11
55− handleShardInput(shard, input) {
12+export default function Shards({ loginCompleted }) {
56− if (shard === SHARDS.SHARD1) {
13+ const { wallet, setUrbitWallet } = useWallet();
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
14
69− handlePointNameInput(pointName) {
15+ const [shard1, setShard1] = useState('');
70− if (pointName.length < 15) {
16+ const [shard2, setShard2] = useState('');
71− this.setState({ pointName });
17+ const [shard3, setShard3] = useState('');
72− }
18+ const [passphrase, setPassphrase] = useState('');
73− }
19+ const [pointName, setPointName] = useState('');
74
20
75− async walletFromShards(shard1, shard2, shard3, pointName, passphrase) {
21+ const walletFromShards = async () => {
76 const s1 = shard1 === '' ? undefined : shard1;
22 const s1 = shard1 === '' ? undefined : shard1;
77 const s2 = shard2 === '' ? undefined : shard2;
23 const s2 = shard2 === '' ? undefined : shard2;
78 const s3 = shard3 === '' ? undefined : shard3;
24 const s3 = shard3 === '' ? undefined : shard3;
@@ -86,129 +32,58 @@ class Shards extends React.Component {
86
32
87 if (ticket !== undefined) {
33 if (ticket !== undefined) {
88 const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
34 const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
89− this.props.setUrbitWallet(Just(uhdw));
35+ setUrbitWallet(Just(uhdw));
90 }
36 }
91− }
37+ };
92−
38+
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];
39+ const shards = [shard1, shard2, shard3];
101− const ready = shards.filter(x => x !== '').length > 1;
40+ const ready = shards.filter(x => x !== '').length > 1;
102−
41+
103− return (
42+ return (
104− <Row>
43+ <View>
105− <Col>
106− <H1>{'Authenticate'}</H1>
107−
108− <P>
109− {`Enter your point and at least two of your three Urbit master
44+ Enter your point and at least two of your three Urbit master ticket shards
110− ticket shards here. The index of the input field needs to
45+ here. The index of the input field needs to match the index of the shard.
111− match the index of the shard.`}
112− </P>
113−
114− <PointInput
46+ <PointInput
115− className="mono mt-8"
47+ name="point"
116− prop-size="lg"
48+ label="Point"
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}
49+ initialValue={pointName}
127− size={68}
50+ onValue={setPointName}
128− margin={8}
51+ autoFocus
129− />
52+ />
130− </PointInput>
53+ <TicketInput
131−
132− <ShardInput
133− className="mono mt-8"
134− prop-size="md"
135− prop-format="innerLabel"
136− type="text"
137− name="shard1"
54+ name="shard1"
138− placeholder={`e.g. ${phTick}`}
55+ label="Shard 1"
139− value={shard1}
56+ initialValue={shard1}
140− onChange={inp => this.handleShardInput(SHARDS.SHARD1, inp)}>
57+ onValue={setShard1}></TicketInput>
141− <InnerLabel>{'Shard 1'}</InnerLabel>
58+ <TicketInput
142− </ShardInput>
143−
144− <ShardInput
145− className="mono mt-8"
146− prop-size="md"
147− prop-format="innerLabel"
148− type="text"
149− name="shard2"
59+ name="shard2"
150− placeholder={`e.g. ${phTick}`}
60+ label="Shard 2"
151− value={shard2}
61+ initialValue={shard2}
152− onChange={inp => this.handleShardInput(SHARDS.SHARD2, inp)}>
62+ onValue={setShard2}></TicketInput>
153− <InnerLabel>{'Shard 2'}</InnerLabel>
63+ <TicketInput
154− </ShardInput>
155−
156− <ShardInput
157− className="mono mt-8"
158− prop-size="md"
159− prop-format="innerLabel"
160− type="text"
161− name="shard3"
64+ name="shard3"
162− placeholder={`e.g. ${phTick}`}
65+ label="Shard 3"
163− value={shard3}
66+ initialValue={shard3}
164− onChange={inp => this.handleShardInput(SHARDS.SHARD3, inp)}>
67+ onValue={setShard3}></TicketInput>
165− <InnerLabel>{'Shard 3'}</InnerLabel>
166− </ShardInput>
167−
168− <InputCaption>
169− {'If your wallet requires a passphrase, you may enter it below.'}
68+ If your wallet requires a passphrase, you may enter it below.
170− </InputCaption>
69+ <PassphraseInput
171−
172− <Input
173− className="pt-8"
174− prop-size="md"
175− prop-format="innerLabel"
176− name="passphrase"
70+ name="passphrase"
177− type="password"
71+ label="(Optional) Wallet Passphrase"
178− value={passphrase}
72+ initialValue={passphrase}
179− autocomplete="off"
73+ onValue={setPassphrase}
180− onChange={this.handlePassphraseInput}>
74+ />
181− <InnerLabel>{'Passphrase'}</InnerLabel>
75+ <ForwardButton
182− </Input>
76+ className="mt3"
183−
184− <Button
185− disabled={!ready}
77+ disabled={!ready}
186− className={'mt-8'}
187− prop-size={'lg wide'}
188− // prop-color={this.buttonTriState(wallet)}
189− onClick={() =>
190− this.walletFromShards(
78+ onClick={walletFromShards}>
191− shard1,
192− shard2,
193− shard3,
194− pointName,
195− passphrase
196− )
197− }>
198− {'Unlock Wallet →'}
79+ Unlock Wallet
199− </Button>
80+ </ForwardButton>
200−
81+ <ForwardButton
201− <Button
202− className={'mt-4'}
82+ className="mt3"
203− prop-size={'xl wide'}
204− disabled={Nothing.hasInstance(wallet)}
83+ disabled={Nothing.hasInstance(wallet)}
205− onClick={this.props.loginCompleted}>
84+ onClick={loginCompleted}>
206− {'Continue →'}
85+ Continue
207− </Button>
86+ </ForwardButton>
208− </Col>
87+ </View>
209− </Row>
210− );
88+ );
211− }
212}
89}
213−
214−export default compose(withWallet)(Shards);
src/views/Login/Ticket.js+112−170
@@ -1,26 +1,18 @@
1import { Just, Nothing } from 'folktale/maybe';
1import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
2+import React, { useState, useEffect } from 'react';
3import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
4import * as ob from 'urbit-ob';
4import * as ob from 'urbit-ob';
5
5
6−import {
6+import View from 'components/View';
7− Button,
7+import { PointInput, TicketInput, PassphraseInput } from 'components/Inputs';
8− Row,
8+import { ForwardButton } from 'components/Buttons';
9− Col,
9+
10− Input,
11− InnerLabel,
12− ValidatedSigil,
13− PointInput,
14− TicketInput,
15−} from '../../components/old/Base';
16−
17−import * as need from '../../lib/need';
10+import * as need from 'lib/need';
18−import { compose } from '../../lib/lib';
19−import { WALLET_TYPES, urbitWalletFromTicket } from '../../lib/wallet';
11+import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
20−
12+
21−import { withWallet } from '../../store/wallet';
22−import { withNetwork } from '../../store/network';
13+import { useNetwork } from 'store/network';
23−import { withPointCursor } from '../../store/pointCursor';
14+import { useWallet } from 'store/wallet';
15+import { usePointCursor } from 'store/pointCursor';
24
16
25//TODO should be part of InputWithStatus component
17//TODO should be part of InputWithStatus component
26const INPUT_STATUS = {
18const INPUT_STATUS = {
@@ -29,174 +21,124 @@ const INPUT_STATUS = {
29 FAIL: Symbol('FAIL'),
21 FAIL: Symbol('FAIL'),
30};
22};
31
23
32−class Ticket extends React.Component {
24+export default function Ticket({ advanced, loginCompleted }) {
33− constructor(props) {
25+ // globals
34− super(props);
26+ const { contracts } = useNetwork();
35−
27+ const { wallet, setWalletType, setUrbitWallet } = useWallet();
36− this.state = {
28+ const { setPointCursor } = usePointCursor();
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
29
61− handleTicketInput(ticket) {
30+ // inputs
62− this.setState({ ticket });
31+ //TODO deduce point name from URL if we can, prefill input if we found it
63− this.verifyTicket(this.state.pointName, ticket, this.state.passphrase);
32+ const [pointName, setPointName] = useState('');
64− }
33+ const [ticket, setTicket] = useState('');
34+ const [passphrase, setPassphrase] = useState('');
65
35
66− handlePassphraseInput(passphrase) {
36+ // display state
67− this.setState({ passphrase });
37+ const [ticketStatus, setTicketStatus] = useState(Nothing());
38+
39+ useEffect(() => {
68− this.verifyTicket(this.state.pointName, this.state.ticket, passphrase);
40+ verifyTicket(pointName, ticket, passphrase);
69− }
41+ return () => {};
42+ }, [pointName, ticket, passphrase]);
70
43
71 //TODO maybe want to do this only on-go, because wallet derivation is slow...
44 //TODO maybe want to do this only on-go, because wallet derivation is slow...
72− async verifyTicket(pointName, ticket, passphrase) {
45+ const verifyTicket = async (pointName, ticket, passphrase) => {
46+ setUrbitWallet(Nothing());
73 if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
47 if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
74− this.props.setUrbitWallet(Nothing());
48+ setTicketStatus(Nothing());
75− this.setState({ ticketStatus: Nothing() });
76 return;
49 return;
77 }
50 }
78− this.setState({ ticketStatus: Just(INPUT_STATUS.SPIN) });
51+ setTicketStatus(Just(INPUT_STATUS.SPIN));
79− const contracts = need.contracts(this.props);
52+ const _contracts = need.contracts(contracts);
80 const pointNumber = ob.patp2dec(pointName);
53 const pointNumber = ob.patp2dec(pointName);
81 const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
54 const uhdw = await urbitWalletFromTicket(ticket, pointName, passphrase);
82 const isOwner = azimuth.azimuth.isOwner(
55 const isOwner = azimuth.azimuth.isOwner(
83− contracts,
56+ _contracts,
84 pointNumber,
57 pointNumber,
85 uhdw.ownership.keys.address
58 uhdw.ownership.keys.address
86 );
59 );
87 const isTransferProxy = azimuth.azimuth.isTransferProxy(
60 const isTransferProxy = azimuth.azimuth.isTransferProxy(
88− contracts,
61+ _contracts,
89 pointNumber,
62 pointNumber,
90 uhdw.ownership.keys.address
63 uhdw.ownership.keys.address
91 );
64 );
92− this.props.setUrbitWallet(Just(uhdw));
65+ setUrbitWallet(Just(uhdw));
93− this.setState({
66+ const newStatus =
94− ticketStatus:
95− (await isOwner) || (await isTransferProxy)
67+ (await isOwner) || (await isTransferProxy)
96− ? Just(INPUT_STATUS.GOOD)
68+ ? INPUT_STATUS.GOOD
97− : Just(INPUT_STATUS.FAIL),
69+ : INPUT_STATUS.FAIL;
98− });
70+ setTicketStatus(Just(newStatus));
99− }
71+ };
100−
72+
101− canContinue() {
73+ const canContinue = () => {
102 // this is our only requirement, since we still want people with
74 // this is our only requirement, since we still want people with
103 // non-standard wallet setups to be able to log in
75 // non-standard wallet setups to be able to log in
104− return Just.hasInstance(this.props.wallet);
76+ return Just.hasInstance(wallet);
105− }
77+ };
106−
78+
107− continue() {
79+ const doContinue = () => {
108− //TODO maybe do on-mount?
109− this.props.setWalletType(WALLET_TYPES.TICKET);
80+ setWalletType(WALLET_TYPES.TICKET);
110− this.props.setPointCursor(Just(ob.patp2dec(this.state.pointName)));
81+ setPointCursor(Just(ob.patp2dec(pointName)));
111− this.props.loginCompleted();
82+ loginCompleted();
112− }
83+ };
113−
84+
114− render() {
115− const pointInput = (
85+ const pointInput = (
116− <PointInput
86+ <PointInput
117− className="mono mt-8"
87+ name="point"
118− prop-size="lg"
88+ label="Point"
119− prop-format="innerLabel"
89+ initialValue={pointName}
120− type="text"
90+ onValue={setPointName}
121− autoFocus
91+ autoFocus
122− value={this.state.pointName}
92+ />
123− onChange={this.handlePointNameInput}>
93+ );
124− <InnerLabel>{'Point name'}</InnerLabel>
94+
125− <ValidatedSigil
95+ //TODO integrate into TicketInput?
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({
96+ const displayTicketStatus = ticketStatus.matchWith({
135− Nothing: () => <span />,
97+ Nothing: () => <span />,
136− Just: status => {
98+ Just: status => {
137− switch (status.value) {
99+ switch (status.value) {
138− case INPUT_STATUS.SPIN:
100+ case INPUT_STATUS.SPIN:
139− return <span>⋯</span>;
101+ return <span>⋯</span>;
140− case INPUT_STATUS.GOOD:
102+ case INPUT_STATUS.GOOD:
141− return <span>✓</span>;
103+ return <span>✓</span>;
142− case INPUT_STATUS.FAIL:
104+ case INPUT_STATUS.FAIL:
143− return <span>𐄂</span>;
105+ return <span>𐄂</span>;
144− default:
106+ default:
145− throw new Error('weird input status ' + status.value);
107+ throw new Error('weird input status ' + status.value);
146− }
108+ }
147− },
109+ },
148− });
110+ });
149−
111+
150− const ticketInput = (
112+ const ticketInput = (
151− //TODO NewTicketInput component, that does dashes etc
152− <TicketInput
113+ <TicketInput
153− className="mono mt-8"
154− prop-size="lg"
155− prop-format="innerLabel"
156− type="password"
157− name="ticket"
114+ name="ticket"
158− value={this.state.ticket}
159− onChange={this.handleTicketInput}>
160− <InnerLabel>{'Master ticket'}</InnerLabel>
115+ label="Master ticket"
161− {ticketStatus}
116+ initialValue={ticket}
162− </TicketInput>
117+ onValue={setTicket}
118+ />
163− );
119+ );
164−
120+
165− const passphraseInput = !this.props.advanced ? null : (
121+ const passphraseInput = !advanced ? null : (
166− <Input
122+ <PassphraseInput
167− className="mt-8"
168− prop-size="md"
169− prop-format="innerLabel"
170− name="passphrase"
123+ name="passphrase"
171− type="password"
124+ label="(Optional) Wallet Passphrase"
172− value={this.state.passphrase}
125+ initialValue={passphrase}
173− autocomplete="off"
126+ onValue={setPassphrase}
174− onChange={this.handlePassphraseInput}>
127+ />
175− <InnerLabel>{'Passphrase'}</InnerLabel>
176− </Input>
177− );
128+ );
178−
129+
179− return (
130+ return (
180− <Row>
131+ <View>
181− <Col>
182− {pointInput}
132+ {pointInput}
183− {ticketInput}
133+ {ticketInput}
184− {passphraseInput}
134+ {passphraseInput}
185−
135+
186− <Button
136+ <ForwardButton
187− className={'mt-10'}
137+ className={'mt3'}
188− disabled={!this.canContinue()}
138+ disabled={!canContinue()}
189− onClick={this.continue}>
139+ onClick={doContinue}>
190− {'Go →'}
140+ Continue
191− </Button>
141+ </ForwardButton>
192− </Col>
142+ </View>
193− </Row>
194− );
143+ );
195− }
196}
144}
197−
198−export default compose(
199− withWallet,
200− withNetwork,
201− withPointCursor
202−)(Ticket);
src/views/Login/Trezor.js+74−115
@@ -1,55 +1,42 @@
1import * as bip32 from 'bip32';
1import * as bip32 from 'bip32';
2−import React from 'react';
2+import React, { useState } from 'react';
3import Maybe from 'folktale/maybe';
3import Maybe from 'folktale/maybe';
4−import { Button } from '../../components/old/Base';
4+import { times } from 'lodash';
5−import { Row, Col, H1, P } from '../../components/old/Base';
6−import {
7− InnerLabel,
8− Input,
9− InnerLabelDropdown,
10−} from '../../components/old/Base';
11import TrezorConnect from 'trezor-connect';
5import TrezorConnect from 'trezor-connect';
12import * as secp256k1 from 'secp256k1';
6import * as secp256k1 from 'secp256k1';
13
7
14−import { TREZOR_PATH } from '../../lib/trezor';
8+import View from 'components/View';
15−import { compose } from '../../lib/lib';
9+import { ForwardButton } from 'components/Buttons';
16−import { withWallet } from '../../store/wallet';
10+import { H1, P, Input } from 'indigo-react';
11+import { InnerLabel, InnerLabelDropdown } from 'components/old/Base';
17
12
18−class Trezor extends React.Component {
13+import { TREZOR_PATH } from 'lib/trezor';
19− constructor(props) {
14+import { useWallet } from 'store/wallet';
20− super(props);
21
15
22− this.state = {
16+const accountOptions = [
23− hdpath: TREZOR_PATH.replace(/x/g, 0),
17+ { title: 'Custom path', value: 'custom' },
24− account: 0,
18+ ...times(20, i => ({ title: `Account #${i + 1}`, value: i })),
25− };
19+];
26
20
27− this.handleAccountSelection = this.handleAccountSelection.bind(this);
21+export default function Trezor({ loginCompleted }) {
28− this.handleHdPathInput = this.handleHdPathInput.bind(this);
22+ const { wallet, setWallet, setWalletHdPath } = useWallet();
29− this.pollDevice = this.pollDevice.bind(this);
30− }
31
23
24+ const [hdPath, setHdPath] = useState(TREZOR_PATH.replace(/x/g, 0));
25+ const [account, setAccount] = useState(0);
26+
32− handleAccountSelection(account) {
27+ const handleAccountSelection = account => {
33− let hdpath = this.state.hdpath;
28+ setAccount(account);
34 if (account !== 'custom') {
29 if (account !== 'custom') {
35− hdpath = TREZOR_PATH.replace(/x/g, account);
30+ setHdPath(TREZOR_PATH.replace(/x/g, account));
36 }
31 }
37− this.setState({ account, hdpath });
38− }
32+ };
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
33
34+ const pollDevice = async () => {
48 TrezorConnect.manifest({
35 TrezorConnect.manifest({
49 email: 'bridge-trezor@urbit.org',
36 email: 'bridge-trezor@urbit.org',
50 appUrl: 'https://github.com/urbit/bridge',
37 appUrl: 'https://github.com/urbit/bridge',
51 });
38 });
52− TrezorConnect.getPublicKey({ path: hdpath }).then(info => {
39+ TrezorConnect.getPublicKey({ path: hdPath }).then(info => {
53 if (info.success === true) {
40 if (info.success === true) {
54 const payload = info.payload;
41 const payload = info.payload;
55 const publicKey = Buffer.from(payload.publicKey, 'hex');
42 const publicKey = Buffer.from(payload.publicKey, 'hex');
@@ -57,90 +44,62 @@ class Trezor extends React.Component {
57 const pub = secp256k1.publicKeyConvert(publicKey, true);
44 const pub = secp256k1.publicKeyConvert(publicKey, true);
58 const hd = bip32.fromPublicKey(pub, chainCode);
45 const hd = bip32.fromPublicKey(pub, chainCode);
59 setWallet(Maybe.Just(hd));
46 setWallet(Maybe.Just(hd));
60− setWalletHdPath(hdpath);
47+ setWalletHdPath(hdPath);
61 } else {
48 } else {
62 setWallet(Maybe.Nothing());
49 setWallet(Maybe.Nothing());
63 }
50 }
64 });
51 });
65− }
52+ };
66−
53+
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;
54+ let accountTitle = accountOptions.find(o => o.value === account).title;
85−
55+
86− const accountSelection = (
56+ const accountSelection = (
87− <InnerLabelDropdown
57+ <InnerLabelDropdown
88− className="mt-8"
58+ className="mt-8"
89− prop-size="md"
59+ prop-size="md"
90− prop-format="innerLabel"
60+ prop-format="innerLabel"
91− options={accountOptions}
61+ options={accountOptions}
92− handleUpdate={handleAccountSelection}
62+ handleUpdate={handleAccountSelection}
93− title={'Account'}
63+ title="Account"
94− currentSelectionTitle={accountTitle}
64+ currentSelectionTitle={accountTitle}
95− fullWidth={true}
65+ fullWidth={true}
66+ />
67+ );
68+
69+ const pathSelection =
70+ account !== 'custom' ? null : (
71+ <Input
72+ className="mt3"
73+ name="hdPath"
74+ label="HD path"
75+ autocomplete="off"
76+ initialValue={hdPath}
77+ onValue={setHdPath}
96 />
78 />
97 );
79 );
98
80
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 (
81+ return (
114− <Row>
82+ <View>
115− <Col className={'measure-md'}>
116− <H1>{'Authenticate With Your Trezor'}</H1>
83+ <H1>Authenticate With Your Trezor</H1>
117−
84+
118− <P>
85+ <P>
119− {`Connect and authenticate to your Trezor. If you'd like
86+ Connect and authenticate to your Trezor. If you'd like to use a custom
120− to use a custom derivation path, you may enter it below.`}
87+ derivation path, you may enter it below.
121− </P>
88+ </P>
122−
89+
123− {accountSelection}
90+ {accountSelection}
124− {pathSelection}
91+ {pathSelection}
125−
92+
126− <Button
127− className={'mt-8'}
128− prop-size={'wide lg'}
129− onClick={this.pollDevice}>
93+ <ForwardButton className="mt3" onClick={pollDevice}>
130− {'Authenticate →'}
94+ Authenticate
131− </Button>
95+ </ForwardButton>
132−
96+
133− <Button
97+ <ForwardButton
134− className={'mt-8'}
98+ className="mt3"
135− prop-size={'wide lg'}
136− disabled={Maybe.Nothing.hasInstance(wallet)}
99+ disabled={Maybe.Nothing.hasInstance(wallet)}
137− onClick={this.props.loginCompleted}>
100+ onClick={loginCompleted}>
138− {'Continue →'}
101+ Continue
139− </Button>
102+ </ForwardButton>
140− </Col>
103+ </View>
141− </Row>
142− );
104+ );
143− }
144}
105}
145−
146−export default compose(withWallet)(Trezor);
src/views/ViewPoint.js+4−11
@@ -12,7 +12,6 @@ import { ROUTE_NAMES } from 'lib/routeNames';
12import View from 'components/View';
12import View from 'components/View';
13import { ForwardButton } from 'components/Buttons';
13import { ForwardButton } from 'components/Buttons';
14import { PointInput } from 'components/Inputs';
14import { PointInput } from 'components/Inputs';
15−import InputSigil from 'components/InputSigil';
16
15
17export default function ViewPoint() {
16export default function ViewPoint() {
18 const history = useHistory();
17 const history = useHistory();
@@ -38,21 +37,15 @@ export default function ViewPoint() {
38 <PointInput
37 <PointInput
39 name="point"
38 name="point"
40 label="Point Name"
39 label="Point Name"
40+ initialValue={pointName}
41 onValue={setPointName}
41 onValue={setPointName}
42+ pass={pass}
42 onPass={setPass}
43 onPass={setPass}
44+ error={error}
43 onError={setError}
45 onError={setError}
46+ focused={focused}
44 onFocus={setFocused}
47 onFocus={setFocused}
45 onEnter={goForward}
48 onEnter={goForward}
46− accessory={
47− <InputSigil
48− patp={pointName}
49− size={68}
50− margin={8}
51− pass={pass}
52− focused={focused}
53− error={error}
54− />
55− }
56 autoFocus
49 autoFocus
57 />
50 />
58
51