on master help sign in
Merge branch 'master' into mino-onboarding-invites
commit687b511178f39c1d15c024ca39b4e27deefb4c04 authorFang <git@fang.io> dateFri 10 May 2019, 22:14 (7 years ago) parent0bf12cdfc3f5a8
16 files changed+323−148
.env+1−1 README.md+2 src/bridge/Bridge.js+7−4 src/bridge/components/Base.js+5 src/bridge/components/HorizontalSelector.js+57 src/bridge/components/InnerLabelDropdown.js+2−2 src/bridge/lib/ledger.js+4−2 src/bridge/lib/trezor.js+2−2 src/bridge/lib/wallet.js+1−1 src/bridge/views/AcceptTransfer.js+3−5 src/bridge/views/IssueChild.js+34−70 src/bridge/views/Ledger.js+86−14 src/bridge/views/Network.js+2−2 src/bridge/views/Point/Actions.js+2−2 src/bridge/views/SetProxy.js+58−29 src/bridge/views/Trezor.js+57−14
.env+1−1
@@ -1,2 +1,2 @@
1−REACT_APP_INFURA_KEY='b7d2af9f01534031ba773374f766ef65'
1+REACT_APP_INFURA_ENDPOINT='b7d2af9f01534031ba773374f766ef65'
22
README.md+2
@@ -1,5 +1,7 @@
11# Bridge
22
3+[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
4+
35An application for interacting with Azimuth.
46
57## Usage
src/bridge/Bridge.js+7−4
@@ -18,7 +18,7 @@ import { BRIDGE_ERROR } from './lib/error'
1818const initWeb3 = (networkType) => {
1919 if (networkType === NETWORK_NAMES.MAINNET) {
2020 const endpoint =
21− `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`
21+ `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_ENDPOINT}`
2222
2323 const provider = new Web3.providers.HttpProvider(endpoint)
2424 const web3 = new Web3(provider)
@@ -251,9 +251,12 @@ class Bridge extends React.Component {
251251 }
252252
253253 setTxnConfirmations(txnHash, txnConfirmations) {
254− this.setState((prevState, _) =>
255− ({txnConfirmations: { ...prevState.txnConfirmations,
256− [txnHash]: txnConfirmations}}))
254+ this.setState((prevState, _) => {
255+ return prevState.routeCrumbs.peek() === ROUTE_NAMES.SENT_TRANSACTION
256+ ? {txnConfirmations: { ...prevState.txnConfirmations,
257+ [txnHash]: txnConfirmations}}
258+ : null
259+ })
257260 }
258261
259262 render() {
src/bridge/components/Base.js+5
@@ -37,6 +37,9 @@ import DropdownItem from './DropdownItem';
3737import DropdownDivider from './DropdownDivider';
3838import InnerLabelDropdown from './InnerLabelDropdown';
3939
40+// Option selectors
41+import HorizontalSelector from './HorizontalSelector';
42+
4043// Path Control
4144import Breadcrumb from './Breadcrumb';
4245import BreadcrumbItem from './BreadcrumbItem';
@@ -183,6 +186,8 @@ export {
183186 DropdownDivider,
184187 InnerLabelDropdown,
185188
189+ HorizontalSelector,
190+
186191 Breadcrumb,
187192 BreadcrumbItem,
188193
src/bridge/components/HorizontalSelector.jsnew file+57
@@ -0,0 +1,57 @@
1+import React from 'react'
2+import PropTypes from 'prop-types';
3+
4+class HorizontalSelector extends React.Component {
5+ constructor(props) {
6+ super(props);
7+
8+ this.state = {
9+ option: this.props.options[0].value
10+ };
11+
12+ this.selectOption = this.selectOption.bind(this);
13+ }
14+
15+ selectOption(opt) {
16+ this.props.onChange(opt);
17+ this.setState({option: opt});
18+ }
19+
20+ getOptionElems() {
21+ return this.props.options.map(opt => {
22+ let active = (opt.value === this.state.option)
23+ ? 'btn-primary active'
24+ : '';
25+ return (
26+ <button
27+ className={`btn ${active} h-10 ph-4 flex sans ${this.props.className}`}
28+ value={opt.value}
29+ onClick={() => this.selectOption(opt.value)}>
30+ { opt.title }
31+ </button>
32+ )
33+ })
34+ }
35+
36+ render() {
37+ const optionElems = this.getOptionElems();
38+
39+ return (
40+ <div className='btn-group'>
41+ { optionElems }
42+ </div>
43+ )
44+ }
45+}
46+
47+HorizontalSelector.propTypes = {
48+ className: PropTypes.string,
49+ options: PropTypes.array
50+};
51+
52+HorizontalSelector.defaultProps = {
53+ className: '',
54+ options: []
55+};
56+
57+export default HorizontalSelector
src/bridge/components/InnerLabelDropdown.js+2−2
@@ -62,14 +62,14 @@ class InnerLabelDropdown extends React.Component {
6262 const fullWidthClass = props.fullWidth ? 'full-width' : ''
6363
6464 return (
65− <div className={`rel table ${fullWidthClass}`}>
65+ <div className={`rel table ${fullWidthClass} ${props.className}`}>
6666 <div
6767 className={`fix full o-0 ${menuVisibility}`}
6868 onClick={this.close}
6969 />
7070
7171 <button
72− className={`h-10 mp-0 bg-white outline-blue sans ${props.className} ${fullWidthClass}`}
72+ className={`h-10 mp-0 bg-white outline-blue sans ${fullWidthClass}`}
7373 onClick={this.toggle}
7474 disabled={props.disabled}>
7575 <div className={'flex'}>
src/bridge/lib/ledger.js+4−2
@@ -1,7 +1,8 @@
11import Transport from '@ledgerhq/hw-transport-u2f'
22import Eth from '@ledgerhq/hw-app-eth'
33
4−const LEDGER_BASE_PATH = "44'/60'/0'/0/0"
4+const LEDGER_LEGACY_PATH = "m/44'/60'/0'/x"
5+const LEDGER_LIVE_PATH = "m/44'/60'/x'/0/0"
56
67const chopHdPrefix = str =>
78 str.slice(0, 2) === "m/"
@@ -24,6 +25,7 @@ const ledgerSignTransaction = async (txn, hdpath) => {
2425}
2526
2627export {
27− LEDGER_BASE_PATH,
28+ LEDGER_LIVE_PATH,
29+ LEDGER_LEGACY_PATH,
2830 ledgerSignTransaction
2931}
src/bridge/lib/trezor.js+2−2
@@ -1,6 +1,6 @@
11import TrezorConnect from 'trezor-connect'
22
3−const TREZOR_BASE_PATH = "m/44'/60'/0'/0/0"
3+const TREZOR_PATH = "m/44'/60'/0'/0/x"
44
55const formatChainId = val =>
66 typeof val === 'number'
@@ -33,7 +33,7 @@ const trezorSignTransaction = async (txn, hdpath) => {
3333}
3434
3535export {
36− TREZOR_BASE_PATH,
36+ TREZOR_PATH,
3737 trezorSignTransaction
3838}
3939
src/bridge/lib/wallet.js+1−1
@@ -72,7 +72,7 @@ const stripHexPrefix = hex =>
7272const keccak256 = str =>
7373 keccak('keccak256').update(str).digest()
7474
75−const isValidAddress = isAddress
75+const isValidAddress = a => (('0x0' === a) || isAddress(a))
7676
7777const toChecksumAddress = (address) => {
7878 const addr = stripHexPrefix(address).toLowerCase()
src/bridge/views/AcceptTransfer.js+3−5
@@ -60,13 +60,11 @@ class AcceptTransfer extends React.Component {
6060 }
6161 })
6262
63− const owner = props.pointCache[validPoint].owner
64−
65− return Just(azimuth.ecliptic.transferFrom(
63+ return Just(azimuth.ecliptic.transferPoint(
6664 validContracts,
67− owner,
65+ validPoint,
6866 state.receivingAddress,
69− validPoint
67+ true
7068 ))
7169 }
7270
src/bridge/views/IssueChild.js+34−70
@@ -20,6 +20,15 @@ import {
2020 eqAddr
2121} from '../lib/wallet'
2222
23+const setFind = (set, pred) => {
24+ for (const e of set) {
25+ if (pred(e)) {
26+ return e
27+ }
28+ }
29+ return undefined
30+}
31+
2332class IssueChild extends React.Component {
2433 constructor(props) {
2534 super(props)
@@ -46,11 +55,12 @@ class IssueChild extends React.Component {
4655 desiredPoint: '',
4756 isAvailable: Nothing(), // use Nothing to allow attempt when offline
4857 suggestions: suggestions,
58+ validChildren: new Set(),
59+ autoComplete: ''
4960 }
5061
5162 this.handlePointInput = this.handlePointInput.bind(this)
5263 this.handleAddressInput = this.handleAddressInput.bind(this)
53− this.handleConfirmAvailability = this.handleConfirmAvailability.bind(this)
5464 this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
5565 this.statelessRef = React.createRef();
5666 }
@@ -60,28 +70,11 @@ class IssueChild extends React.Component {
6070 this.statelessRef.current.clearTxn()
6171 }
6272
63− handlePointInput(desiredPoint) {
64− if (desiredPoint.length < 15) {
65− this.setState({
66− desiredPoint,
67− isAvailable: Nothing()
68− })
69− this.statelessRef.current.clearTxn()
70− }
71− }
73+ componentWillMount() {
7274
73− handleConfirmAvailability() {
74− this.confirmPointAvailability().then(r => {
75− this.setState({
76− isAvailable: r,
77− })
78− })
79−
80− }
81−
82− async confirmPointAvailability() {
8375 const { contracts } = this.props
84− const { desiredPoint } = this.state
76+ const { issuingPoint } = this.state
77+
8578
8679 const validContracts = contracts.matchWith({
8780 Just: cs => cs.value,
@@ -90,31 +83,28 @@ class IssueChild extends React.Component {
9083 }
9184 })
9285
93− if (canDecodePatp(desiredPoint) === false) {
94− return Just(false)
95− }
86+ azimuth.getUnspawnedChildren(validContracts, issuingPoint).then(ps => this.setState({ validChildren: new Set(ps.map(ob.patp)) }))
87+ }
9688
97− const point = ob.patp2dec(desiredPoint)
98− const pointSize = azimuth.getPointSize(point)
89+ handlePointInput(desiredPoint) {
90+ if (desiredPoint.length < 15) {
9991
100− const prefix = azimuth.getPrefix(point)
101− const prefixSize = azimuth.getPointSize(prefix)
92+ const suggestedPoint = setFind(this.state.validChildren, (e => e.startsWith(desiredPoint))) || ''
93+ const rendered = ' '.repeat(desiredPoint.length) + suggestedPoint.substring(desiredPoint.length)
10294
103− // NB (jtobin): this prevents galaxies from attempting to spawn planets
104− //
105− if (pointSize !== prefixSize + 1) {
106− return Just(false)
107− }
95+ const available = Just(this.state.validChildren.has(desiredPoint))
10896
109− const owner = await azimuth.getOwner(validContracts, point)
97+ this.setState({
98+ desiredPoint,
99+ isAvailable: available,
100+ autoComplete: rendered
101+ })
102+ this.statelessRef.current.clearTxn()
110103
111− if (eqAddr(owner, ETH_ZERO_ADDR)) {
112− return Just(true)
113104 }
114−
115− return Just(false)
116105 }
117106
107+
118108 createUnsignedTxn() {
119109 const { state, props } = this
120110
@@ -140,19 +130,6 @@ class IssueChild extends React.Component {
140130 return Just(txn)
141131 }
142132
143− buttonTriState() {
144− const a = this.state.isAvailable
145− if (Nothing.hasInstance(a)) return 'blue'
146− if (a.value === false) return 'yellow'
147− if (a.value === true) return 'green'
148− }
149−
150− buttonTriStateText() {
151− const a = this.state.isAvailable
152− if (Nothing.hasInstance(a)) return 'Confirm Availablility'
153− if (a.value === false) return 'Point is Not Available'
154− if (a.value === true) return 'Available'
155− }
156133
157134 validatePoint = patp => {
158135 const point = this.point
@@ -176,16 +153,13 @@ class IssueChild extends React.Component {
176153 const { props, state } = this
177154
178155 const validAddress = isValidAddress(state.receivingAddress)
179− const validPoint = canDecodePatp(state.desiredPoint)
180156
181157 const canGenerate = props.web3.matchWith({
182158 Nothing: () => {
183− return validAddress === true &&
184− validPoint === true
159+ return validAddress === true
185160 },
186161 Just: () => {
187162 return validAddress === true &&
188− validPoint === true &&
189163 state.isAvailable.value === true
190164 }
191165 })
@@ -250,6 +224,11 @@ class IssueChild extends React.Component {
250224 value={ state.desiredPoint }
251225 onChange={ this.handlePointInput }>
252226 <InnerLabel>{ 'Point to Issue' }</InnerLabel>
227+ <div
228+ style={{marginTop: '8.8rem', marginLeft: '4.6rem', fontSize: '5rem', color: '#757575', whiteSpace: 'pre-wrap'}}
229+ className='abs tl-0 mono'>
230+ { state.autoComplete }
231+ </div>
253232 <ValidatedSigil
254233 className={'tr-0 mt-05 mr-0 abs'}
255234 patp={state.desiredPoint}
@@ -279,21 +258,6 @@ class IssueChild extends React.Component {
279258 {'View on Etherscan ↗'}
280259 </Anchor>
281260
282− {
283− props.web3.matchWith({
284− Nothing: _ => <div />,
285− Just: _ =>
286− <Button
287− prop-size='lg wide'
288− prop-color={this.buttonTriState()}
289− className={'mt-8'}
290− disabled={!validPoint}
291− onClick={this.handleConfirmAvailability}>
292− {this.buttonTriStateText()}
293− </Button>
294− })
295− }
296−
297261 <StatelessTransaction
298262 // Upper scope
299263 web3={props.web3}
src/bridge/views/Ledger.js+86−14
@@ -3,12 +3,12 @@ import React from 'react'
33import Maybe from 'folktale/maybe'
44import { Button } from '../components/Base'
55import { Row, Col, H1, P, H2 } from '../components/Base'
6−import { Input, InnerLabel } from '../components/Base'
6+import { Input, InnerLabel, InnerLabelDropdown } from '../components/Base'
77import Transport from '@ledgerhq/hw-transport-u2f'
88import Eth from '@ledgerhq/hw-app-eth'
99import * as secp256k1 from 'secp256k1'
1010
11−import { LEDGER_BASE_PATH } from '../lib/ledger'
11+import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from '../lib/ledger'
1212import { ROUTE_NAMES } from '../lib/router'
1313
1414const chopHdPrefix = str =>
@@ -26,11 +26,32 @@ class Ledger extends React.Component {
2626 super(props)
2727
2828 this.state = {
29− hdpath: LEDGER_BASE_PATH
29+ basePath: LEDGER_LIVE_PATH,
30+ hdpath: LEDGER_LIVE_PATH.replace(/x/g, 0),
31+ account: 0
3032 }
3133
3234 this.pollDevice = this.pollDevice.bind(this)
3335 this.handleHdPathInput = this.handleHdPathInput.bind(this)
36+ this.handlePathSelection = this.handlePathSelection.bind(this);
37+ this.handleAccountSelection = this.handleAccountSelection.bind(this);
38+ this.updateHdPath = this.updateHdPath.bind(this);
39+ }
40+
41+ handlePathSelection(basePath) {
42+ this.updateHdPath(basePath, this.state.account);
43+ }
44+
45+ handleAccountSelection(account) {
46+ this.updateHdPath(this.state.basePath, account);
47+ }
48+
49+ updateHdPath(basePath, account) {
50+ let hdpath = this.state.hdpath;
51+ if (basePath !== 'custom') {
52+ hdpath = basePath.replace(/x/g, account);
53+ }
54+ this.setState({ basePath, account, hdpath });
3455 }
3556
3657 handleHdPathInput(hdpath) {
@@ -60,7 +81,66 @@ class Ledger extends React.Component {
6081
6182 render() {
6283 const { pushRoute, popRoute, wallet } = this.props
63− const { hdpath } = this.state
84+ const { basePath, hdpath, account } = this.state;
85+ const { handlePathSelection, handleAccountSelection } = this;
86+
87+ const pathOptions = [
88+ { title: 'Ledger Live',
89+ value: LEDGER_LIVE_PATH
90+ },
91+ { title: 'Ledger Legacy',
92+ value: LEDGER_LEGACY_PATH
93+ },
94+ {
95+ title: 'Custom path',
96+ value: 'custom'
97+ }
98+ ];
99+ let basePathTitle = pathOptions.find(o => (o.value === basePath)).title;
100+
101+ let accountOptions = [];
102+ for (let i = 0; i < 20; i++) {
103+ accountOptions.push({
104+ title: 'Account #' + (i+1),
105+ value: i
106+ });
107+ }
108+ let accountTitle = accountOptions.find(o => (o.value === account)).title;
109+
110+ const basePathSelection = (
111+ <InnerLabelDropdown
112+ className='mt-8'
113+ options={pathOptions}
114+ handleUpdate={handlePathSelection}
115+ title={'Derivation path'}
116+ currentSelectionTitle={basePathTitle}
117+ fullWidth={true}
118+ >
119+ </InnerLabelDropdown>
120+ );
121+
122+ const truePathSelection = (basePath === 'custom')
123+ ? ( <Input
124+ className='pt-8 mt-4 text-mono'
125+ prop-size='md'
126+ prop-format='innerLabel'
127+ name='hdpath'
128+ value={ addHdPrefix(hdpath) }
129+ autocomplete='off'
130+ onChange={ this.handleHdPathInput }>
131+ <InnerLabel>{'HD Path'}</InnerLabel>
132+ </Input> )
133+ : ( <InnerLabelDropdown
134+ className='mt-4'
135+ prop-size='md'
136+ prop-format='innerLabel'
137+ options={accountOptions}
138+ handleUpdate={handleAccountSelection}
139+ title={'Account'}
140+ currentSelectionTitle={accountTitle}
141+ fullWidth={true}
142+ >
143+ </InnerLabelDropdown> );
64144
65145 return (
66146
@@ -99,16 +179,8 @@ class Ledger extends React.Component {
99179 <li className={'mt-4'}>{'Run '}<code>{'python bridge-https.py'}</code></li>
100180 </ol>
101181
102− <Input
103− className='pt-8 mt-8 text-mono'
104− prop-size='md'
105− prop-format='innerLabel'
106− name='hdpath'
107− value={ addHdPrefix(hdpath) }
108− autocomplete='off'
109− onChange={ this.handleHdPathInput }>
110− <InnerLabel>{'HD Path'}</InnerLabel>
111− </Input>
182+ { basePathSelection }
183+ { truePathSelection }
112184
113185 <Button
114186 prop-size={'wide lg'}
src/bridge/views/Network.js+2−2
@@ -36,7 +36,7 @@ class Network extends React.Component {
3636 setNetworkType(network)
3737
3838 const endpoint =
39− `https://ropsten.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`
39+ `https://ropsten.infura.io/v3/${process.env.REACT_APP_INFURA_ENDPOINT}`
4040
4141 const provider = new Web3.providers.HttpProvider(endpoint)
4242 const web3 = new Web3(provider)
@@ -49,7 +49,7 @@ class Network extends React.Component {
4949 setNetworkType(network)
5050
5151 const endpoint =
52− `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`
52+ `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_ENDPOINT}`
5353
5454 const provider = new Web3.providers.HttpProvider(endpoint)
5555 const web3 = new Web3(provider)
src/bridge/views/Point/Actions.js+2−2
@@ -215,7 +215,7 @@ const Actions = (props) => {
215215 pushRoute(ROUTE_NAMES.SET_SPAWN_PROXY)
216216 }}
217217 >
218− { 'Set spawn proxy' }
218+ { 'Change spawn proxy' }
219219 </Button>
220220
221221 <Button
@@ -226,7 +226,7 @@ const Actions = (props) => {
226226 pushRoute(ROUTE_NAMES.SET_MANAGEMENT_PROXY)
227227 }}
228228 >
229− { 'Set management proxy' }
229+ { 'Change management proxy' }
230230 </Button>
231231
232232 <Button
src/bridge/views/SetProxy.js+58−29
@@ -1,7 +1,7 @@
11import Maybe from 'folktale/maybe'
22import React from 'react'
33import * as azimuth from 'azimuth-js'
4−import { Row, Col, H1, P, InnerLabel, ShowBlockie, Anchor } from '../components/Base'
4+import { Row, Col, H1, P, InnerLabel, ShowBlockie, Anchor, HorizontalSelector } from '../components/Base'
55import { AddressInput } from '../components/Base'
66import * as ob from 'urbit-ob'
77
@@ -46,13 +46,23 @@ class SetProxy extends React.Component {
4646 this.state = {
4747 proxyAddress: '',
4848 issuingPoint: issuingPoint,
49+ setProxy: true
4950 }
5051
5152 this.statelessRef = React.createRef();
53+ this.handleSetUnset = this.handleSetUnset.bind(this);
5254 this.handleAddressInput = this.handleAddressInput.bind(this)
5355 this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
5456 }
5557
58+ handleSetUnset(selected) {
59+ const set = (selected === 'set');
60+ this.setState({
61+ setProxy: set,
62+ proxyAddress: set ? '' : '0x0000000000000000000000000000000000000000'
63+ });
64+ }
65+
5666 handleAddressInput(proxyAddress) {
5767 this.setState({ proxyAddress })
5868
@@ -116,42 +126,61 @@ class SetProxy extends React.Component {
116126
117127 const ucFirst = w => w.charAt(0).toUpperCase() + w.slice(1);
118128
129+ const setUnset = [
130+ { title: 'Set', value: 'set' },
131+ { title: 'Unset', value: 'unset' }
132+ ];
133+
134+ const titleVerb = this.state.setProxy ? 'Set' : 'Unset';
135+
136+ let addressInput = null;
137+ if (this.state.setProxy) {
138+ addressInput = (<>
139+ <P className='mt-8'>
140+ {
141+ 'Please provide an Ethereum address to act as the ' +
142+ `${renderedProxyType} proxy. You can also use Wallet ` +
143+ 'Generator to generate a keypair.'
144+ }
145+ </P>
146+
147+ <AddressInput
148+ className='mono mt-8'
149+ prop-size='lg'
150+ prop-format='innerLabel'
151+ placeholder={ `e.g. 0x84295d5e054d8cff5a22428b195f5a1615bd644f` }
152+ value={ state.proxyAddress }
153+ onChange={ v => this.handleAddressInput(v) }>
154+ <InnerLabel>{ 'Proxy Address' }</InnerLabel>
155+ <ShowBlockie className={'mt-1'} address={state.proxyAddress} />
156+ </AddressInput>
157+
158+ <Anchor
159+ className={'mt-1'}
160+ prop-size={'sm'}
161+ prop-disabled={!isValidAddress(state.proxyAddress) || !esvisible}
162+ target={'_blank'}
163+ href={`https://${esdomain}/address/${state.proxyAddress}`}>
164+ {'View on Etherscan ↗'}
165+ </Anchor>
166+ </>)
167+ }
119168
120169 return (
121170 <Row>
122171 <Col>
123172 <H1>
124− { `Set ${ucFirst(renderedProxyType)} Proxy For ` }
173+ { `${titleVerb} ${ucFirst(renderedProxyType)} Proxy For ` }
125174 <code>{ `${ob.patp(state.issuingPoint)}` }</code>
126175 </H1>
127176
128− <P>
129− {
130− 'Please provide an Ethereum address to act as the ' +
131− `${renderedProxyType} proxy. You can also use Wallet ` +
132− 'Generator to generate a keypair.'
133− }
134− </P>
135−
136− <AddressInput
137− className='mono mt-8'
138− prop-size='lg'
139− prop-format='innerLabel'
140− placeholder={ `e.g. 0x84295d5e054d8cff5a22428b195f5a1615bd644f` }
141− value={ state.proxyAddress }
142− onChange={ v => this.handleAddressInput(v) }>
143− <InnerLabel>{ 'Proxy Address' }</InnerLabel>
144− <ShowBlockie className={'mt-1'} address={state.proxyAddress} />
145− </AddressInput>
146−
147− <Anchor
148− className={'mt-1'}
149− prop-size={'sm'}
150− prop-disabled={!isValidAddress(state.proxyAddress) || !esvisible}
151− target={'_blank'}
152− href={`https://${esdomain}/address/${state.proxyAddress}`}>
153− {'View on Etherscan ↗'}
154− </Anchor>
177+ <HorizontalSelector
178+ options={setUnset}
179+ onChange={this.handleSetUnset}
180+ className='mt-8'
181+ />
182+
183+ { addressInput }
155184
156185 <StatelessTransaction
157186 // Upper scope
src/bridge/views/Trezor.js+57−14
@@ -3,11 +3,11 @@ import React from 'react'
33import Maybe from 'folktale/maybe'
44import { Button } from '../components/Base'
55import { Row, Col, H1, P } from '../components/Base'
6−import { InnerLabel, Input } from '../components/Base'
6+import { InnerLabel, Input, InnerLabelDropdown } from '../components/Base'
77import TrezorConnect from 'trezor-connect'
88import * as secp256k1 from 'secp256k1'
99
10−import { TREZOR_BASE_PATH } from '../lib/trezor'
10+import { TREZOR_PATH } from '../lib/trezor'
1111import { ROUTE_NAMES } from '../lib/router'
1212
1313class Trezor extends React.Component {
@@ -16,13 +16,23 @@ class Trezor extends React.Component {
1616 super(props)
1717
1818 this.state = {
19− hdpath: TREZOR_BASE_PATH
19+ hdpath: TREZOR_PATH.replace(/x/g, 0),
20+ account: 0
2021 }
2122
23+ this.handleAccountSelection = this.handleAccountSelection.bind(this)
2224 this.handleHdPathInput = this.handleHdPathInput.bind(this)
2325 this.pollDevice = this.pollDevice.bind(this)
2426 }
2527
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+
2636 handleHdPathInput(hdpath) {
2737 this.setState({ hdpath })
2838 }
@@ -53,7 +63,48 @@ class Trezor extends React.Component {
5363
5464 render() {
5565 const { pushRoute, popRoute, wallet } = this.props
56− const { hdpath } = this.state
66+ const { hdpath, account } = this.state
67+ const { handleAccountSelection } = this;
68+
69+ let accountOptions = [{
70+ title: 'Custom path',
71+ value: 'custom'
72+ }];
73+ for (let i = 0; i < 20; i++) {
74+ accountOptions.push({
75+ title: 'Account #' + (i+1),
76+ value: i
77+ });
78+ }
79+ let accountTitle = accountOptions.find(o => (o.value === account)).title;
80+
81+ const accountSelection = (
82+ <InnerLabelDropdown
83+ className='mt-8'
84+ prop-size='md'
85+ prop-format='innerLabel'
86+ options={accountOptions}
87+ handleUpdate={handleAccountSelection}
88+ title={'Account'}
89+ currentSelectionTitle={accountTitle}
90+ fullWidth={true}
91+ >
92+ </InnerLabelDropdown>
93+ );
94+
95+ const pathSelection = (account !== 'custom') ? null :
96+ (
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+ );
57108
58109 return (
59110 <Row>
@@ -66,16 +117,8 @@ class Trezor extends React.Component {
66117 }
67118 </P>
68119
69− <Input
70− className='pt-8 text-mono'
71− prop-size='md'
72− prop-format='innerLabel'
73− name='hdpath'
74− value={ hdpath }
75− autocomplete='off'
76− onChange={ this.handleHdPathInput }>
77− <InnerLabel>{'HD Path'}</InnerLabel>
78− </Input>
120+ { accountSelection }
121+ { pathSelection }
79122
80123 <Button
81124 className={'mt-8'}