on master help sign in
Merge branch 'master' into mino-onboarding-invites
687b511Fang
@@ -1,2 +1,2 @@
1−REACT_APP_INFURA_KEY='b7d2af9f01534031ba773374f766ef65'
1+REACT_APP_INFURA_ENDPOINT='b7d2af9f01534031ba773374f766ef65'
2
2
README.md+2
@@ -1,5 +1,7 @@
1# Bridge
1# Bridge
2
2
3+[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
4+
3An application for interacting with Azimuth.
5An application for interacting with Azimuth.
4
6
5## Usage
7## Usage
src/bridge/Bridge.js+7−4
@@ -18,7 +18,7 @@ import { BRIDGE_ERROR } from './lib/error'
18const initWeb3 = (networkType) => {
18const initWeb3 = (networkType) => {
19 if (networkType === NETWORK_NAMES.MAINNET) {
19 if (networkType === NETWORK_NAMES.MAINNET) {
20 const endpoint =
20 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}`
22
22
23 const provider = new Web3.providers.HttpProvider(endpoint)
23 const provider = new Web3.providers.HttpProvider(endpoint)
24 const web3 = new Web3(provider)
24 const web3 = new Web3(provider)
@@ -251,9 +251,12 @@ class Bridge extends React.Component {
251 }
251 }
252
252
253 setTxnConfirmations(txnHash, txnConfirmations) {
253 setTxnConfirmations(txnHash, txnConfirmations) {
254− this.setState((prevState, _) =>
254+ this.setState((prevState, _) => {
255+ return prevState.routeCrumbs.peek() === ROUTE_NAMES.SENT_TRANSACTION
255− ({txnConfirmations: { ...prevState.txnConfirmations,
256+ ? {txnConfirmations: { ...prevState.txnConfirmations,
256− [txnHash]: txnConfirmations}}))
257+ [txnHash]: txnConfirmations}}
258+ : null
259+ })
257 }
260 }
258
261
259 render() {
262 render() {
src/bridge/components/Base.js+5
@@ -37,6 +37,9 @@ import DropdownItem from './DropdownItem';
37import DropdownDivider from './DropdownDivider';
37import DropdownDivider from './DropdownDivider';
38import InnerLabelDropdown from './InnerLabelDropdown';
38import InnerLabelDropdown from './InnerLabelDropdown';
39
39
40+// Option selectors
41+import HorizontalSelector from './HorizontalSelector';
42+
40// Path Control
43// Path Control
41import Breadcrumb from './Breadcrumb';
44import Breadcrumb from './Breadcrumb';
42import BreadcrumbItem from './BreadcrumbItem';
45import BreadcrumbItem from './BreadcrumbItem';
@@ -183,6 +186,8 @@ export {
183 DropdownDivider,
186 DropdownDivider,
184 InnerLabelDropdown,
187 InnerLabelDropdown,
185
188
189+ HorizontalSelector,
190+
186 Breadcrumb,
191 Breadcrumb,
187 BreadcrumbItem,
192 BreadcrumbItem,
188
193
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 {
62 const fullWidthClass = props.fullWidth ? 'full-width' : ''
62 const fullWidthClass = props.fullWidth ? 'full-width' : ''
63
63
64 return (
64 return (
65− <div className={`rel table ${fullWidthClass}`}>
65+ <div className={`rel table ${fullWidthClass} ${props.className}`}>
66 <div
66 <div
67 className={`fix full o-0 ${menuVisibility}`}
67 className={`fix full o-0 ${menuVisibility}`}
68 onClick={this.close}
68 onClick={this.close}
69 />
69 />
70
70
71 <button
71 <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}`}
73 onClick={this.toggle}
73 onClick={this.toggle}
74 disabled={props.disabled}>
74 disabled={props.disabled}>
75 <div className={'flex'}>
75 <div className={'flex'}>
src/bridge/lib/ledger.js+4−2
@@ -1,7 +1,8 @@
1import Transport from '@ledgerhq/hw-transport-u2f'
1import Transport from '@ledgerhq/hw-transport-u2f'
2import Eth from '@ledgerhq/hw-app-eth'
2import Eth from '@ledgerhq/hw-app-eth'
3
3
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"
5
6
6const chopHdPrefix = str =>
7const chopHdPrefix = str =>
7 str.slice(0, 2) === "m/"
8 str.slice(0, 2) === "m/"
@@ -24,6 +25,7 @@ const ledgerSignTransaction = async (txn, hdpath) => {
24}
25}
25
26
26export {
27export {
27− LEDGER_BASE_PATH,
28+ LEDGER_LIVE_PATH,
29+ LEDGER_LEGACY_PATH,
28 ledgerSignTransaction
30 ledgerSignTransaction
29}
31}
src/bridge/lib/trezor.js+2−2
@@ -1,6 +1,6 @@
1import TrezorConnect from 'trezor-connect'
1import TrezorConnect from 'trezor-connect'
2
2
3−const TREZOR_BASE_PATH = "m/44'/60'/0'/0/0"
3+const TREZOR_PATH = "m/44'/60'/0'/0/x"
4
4
5const formatChainId = val =>
5const formatChainId = val =>
6 typeof val === 'number'
6 typeof val === 'number'
@@ -33,7 +33,7 @@ const trezorSignTransaction = async (txn, hdpath) => {
33}
33}
34
34
35export {
35export {
36− TREZOR_BASE_PATH,
36+ TREZOR_PATH,
37 trezorSignTransaction
37 trezorSignTransaction
38}
38}
39
39
src/bridge/lib/wallet.js+1−1
@@ -72,7 +72,7 @@ const stripHexPrefix = hex =>
72const keccak256 = str =>
72const keccak256 = str =>
73 keccak('keccak256').update(str).digest()
73 keccak('keccak256').update(str).digest()
74
74
75−const isValidAddress = isAddress
75+const isValidAddress = a => (('0x0' === a) || isAddress(a))
76
76
77const toChecksumAddress = (address) => {
77const toChecksumAddress = (address) => {
78 const addr = stripHexPrefix(address).toLowerCase()
78 const addr = stripHexPrefix(address).toLowerCase()
src/bridge/views/AcceptTransfer.js+3−5
@@ -60,13 +60,11 @@ class AcceptTransfer extends React.Component {
60 }
60 }
61 })
61 })
62
62
63− const owner = props.pointCache[validPoint].owner
64−
65− return Just(azimuth.ecliptic.transferFrom(
63+ return Just(azimuth.ecliptic.transferPoint(
66 validContracts,
64 validContracts,
67− owner,
65+ validPoint,
68 state.receivingAddress,
66 state.receivingAddress,
69− validPoint
67+ true
70 ))
68 ))
71 }
69 }
72
70
src/bridge/views/IssueChild.js+34−70
@@ -20,6 +20,15 @@ import {
20 eqAddr
20 eqAddr
21} from '../lib/wallet'
21} from '../lib/wallet'
22
22
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+
23class IssueChild extends React.Component {
32class IssueChild extends React.Component {
24 constructor(props) {
33 constructor(props) {
25 super(props)
34 super(props)
@@ -46,11 +55,12 @@ class IssueChild extends React.Component {
46 desiredPoint: '',
55 desiredPoint: '',
47 isAvailable: Nothing(), // use Nothing to allow attempt when offline
56 isAvailable: Nothing(), // use Nothing to allow attempt when offline
48 suggestions: suggestions,
57 suggestions: suggestions,
58+ validChildren: new Set(),
59+ autoComplete: ''
49 }
60 }
50
61
51 this.handlePointInput = this.handlePointInput.bind(this)
62 this.handlePointInput = this.handlePointInput.bind(this)
52 this.handleAddressInput = this.handleAddressInput.bind(this)
63 this.handleAddressInput = this.handleAddressInput.bind(this)
53− this.handleConfirmAvailability = this.handleConfirmAvailability.bind(this)
54 this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
64 this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
55 this.statelessRef = React.createRef();
65 this.statelessRef = React.createRef();
56 }
66 }
@@ -60,28 +70,11 @@ class IssueChild extends React.Component {
60 this.statelessRef.current.clearTxn()
70 this.statelessRef.current.clearTxn()
61 }
71 }
62
72
63− handlePointInput(desiredPoint) {
73+ componentWillMount() {
64− if (desiredPoint.length < 15) {
65− this.setState({
66− desiredPoint,
67− isAvailable: Nothing()
68− })
69− this.statelessRef.current.clearTxn()
70− }
71− }
72
74
73− handleConfirmAvailability() {
74− this.confirmPointAvailability().then(r => {
75− this.setState({
76− isAvailable: r,
77− })
78− })
79−
80− }
81−
82− async confirmPointAvailability() {
83 const { contracts } = this.props
75 const { contracts } = this.props
84− const { desiredPoint } = this.state
76+ const { issuingPoint } = this.state
77+
85
78
86 const validContracts = contracts.matchWith({
79 const validContracts = contracts.matchWith({
87 Just: cs => cs.value,
80 Just: cs => cs.value,
@@ -90,31 +83,28 @@ class IssueChild extends React.Component {
90 }
83 }
91 })
84 })
92
85
93− if (canDecodePatp(desiredPoint) === false) {
86+ azimuth.getUnspawnedChildren(validContracts, issuingPoint).then(ps => this.setState({ validChildren: new Set(ps.map(ob.patp)) }))
94− return Just(false)
95− }
87+ }
96
88
97− const point = ob.patp2dec(desiredPoint)
89+ handlePointInput(desiredPoint) {
98− const pointSize = azimuth.getPointSize(point)
90+ if (desiredPoint.length < 15) {
99
91
100− const prefix = azimuth.getPrefix(point)
92+ const suggestedPoint = setFind(this.state.validChildren, (e => e.startsWith(desiredPoint))) || ''
101− const prefixSize = azimuth.getPointSize(prefix)
93+ const rendered = ' '.repeat(desiredPoint.length) + suggestedPoint.substring(desiredPoint.length)
102
94
103− // NB (jtobin): this prevents galaxies from attempting to spawn planets
95+ const available = Just(this.state.validChildren.has(desiredPoint))
104− //
105− if (pointSize !== prefixSize + 1) {
106− return Just(false)
107− }
108
96
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()
110
103
111− if (eqAddr(owner, ETH_ZERO_ADDR)) {
112− return Just(true)
113 }
104 }
114−
115− return Just(false)
116 }
105 }
117
106
107+
118 createUnsignedTxn() {
108 createUnsignedTxn() {
119 const { state, props } = this
109 const { state, props } = this
120
110
@@ -140,19 +130,6 @@ class IssueChild extends React.Component {
140 return Just(txn)
130 return Just(txn)
141 }
131 }
142
132
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− }
156
133
157 validatePoint = patp => {
134 validatePoint = patp => {
158 const point = this.point
135 const point = this.point
@@ -176,16 +153,13 @@ class IssueChild extends React.Component {
176 const { props, state } = this
153 const { props, state } = this
177
154
178 const validAddress = isValidAddress(state.receivingAddress)
155 const validAddress = isValidAddress(state.receivingAddress)
179− const validPoint = canDecodePatp(state.desiredPoint)
180
156
181 const canGenerate = props.web3.matchWith({
157 const canGenerate = props.web3.matchWith({
182 Nothing: () => {
158 Nothing: () => {
183− return validAddress === true &&
159+ return validAddress === true
184− validPoint === true
185 },
160 },
186 Just: () => {
161 Just: () => {
187 return validAddress === true &&
162 return validAddress === true &&
188− validPoint === true &&
189 state.isAvailable.value === true
163 state.isAvailable.value === true
190 }
164 }
191 })
165 })
@@ -250,6 +224,11 @@ class IssueChild extends React.Component {
250 value={ state.desiredPoint }
224 value={ state.desiredPoint }
251 onChange={ this.handlePointInput }>
225 onChange={ this.handlePointInput }>
252 <InnerLabel>{ 'Point to Issue' }</InnerLabel>
226 <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>
253 <ValidatedSigil
232 <ValidatedSigil
254 className={'tr-0 mt-05 mr-0 abs'}
233 className={'tr-0 mt-05 mr-0 abs'}
255 patp={state.desiredPoint}
234 patp={state.desiredPoint}
@@ -279,21 +258,6 @@ class IssueChild extends React.Component {
279 {'View on Etherscan ↗'}
258 {'View on Etherscan ↗'}
280 </Anchor>
259 </Anchor>
281
260
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−
297 <StatelessTransaction
261 <StatelessTransaction
298 // Upper scope
262 // Upper scope
299 web3={props.web3}
263 web3={props.web3}
src/bridge/views/Ledger.js+86−14
@@ -3,12 +3,12 @@ import React from 'react'
3import Maybe from 'folktale/maybe'
3import Maybe from 'folktale/maybe'
4import { Button } from '../components/Base'
4import { Button } from '../components/Base'
5import { Row, Col, H1, P, H2 } from '../components/Base'
5import { Row, Col, H1, P, H2 } from '../components/Base'
6−import { Input, InnerLabel } from '../components/Base'
6+import { Input, InnerLabel, InnerLabelDropdown } from '../components/Base'
7import Transport from '@ledgerhq/hw-transport-u2f'
7import Transport from '@ledgerhq/hw-transport-u2f'
8import Eth from '@ledgerhq/hw-app-eth'
8import Eth from '@ledgerhq/hw-app-eth'
9import * as secp256k1 from 'secp256k1'
9import * as secp256k1 from 'secp256k1'
10
10
11−import { LEDGER_BASE_PATH } from '../lib/ledger'
11+import { LEDGER_LIVE_PATH, LEDGER_LEGACY_PATH } from '../lib/ledger'
12import { ROUTE_NAMES } from '../lib/router'
12import { ROUTE_NAMES } from '../lib/router'
13
13
14const chopHdPrefix = str =>
14const chopHdPrefix = str =>
@@ -26,11 +26,32 @@ class Ledger extends React.Component {
26 super(props)
26 super(props)
27
27
28 this.state = {
28 this.state = {
29− hdpath: LEDGER_BASE_PATH
29+ basePath: LEDGER_LIVE_PATH,
30+ hdpath: LEDGER_LIVE_PATH.replace(/x/g, 0),
31+ account: 0
30 }
32 }
31
33
32 this.pollDevice = this.pollDevice.bind(this)
34 this.pollDevice = this.pollDevice.bind(this)
33 this.handleHdPathInput = this.handleHdPathInput.bind(this)
35 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 });
34 }
55 }
35
56
36 handleHdPathInput(hdpath) {
57 handleHdPathInput(hdpath) {
@@ -60,7 +81,66 @@ class Ledger extends React.Component {
60
81
61 render() {
82 render() {
62 const { pushRoute, popRoute, wallet } = this.props
83 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> );
64
144
65 return (
145 return (
66
146
@@ -99,16 +179,8 @@ class Ledger extends React.Component {
99 <li className={'mt-4'}>{'Run '}<code>{'python bridge-https.py'}</code></li>
179 <li className={'mt-4'}>{'Run '}<code>{'python bridge-https.py'}</code></li>
100 </ol>
180 </ol>
101
181
102− <Input
182+ { basePathSelection }
103− className='pt-8 mt-8 text-mono'
183+ { truePathSelection }
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>
112
184
113 <Button
185 <Button
114 prop-size={'wide lg'}
186 prop-size={'wide lg'}
src/bridge/views/Network.js+2−2
@@ -36,7 +36,7 @@ class Network extends React.Component {
36 setNetworkType(network)
36 setNetworkType(network)
37
37
38 const endpoint =
38 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}`
40
40
41 const provider = new Web3.providers.HttpProvider(endpoint)
41 const provider = new Web3.providers.HttpProvider(endpoint)
42 const web3 = new Web3(provider)
42 const web3 = new Web3(provider)
@@ -49,7 +49,7 @@ class Network extends React.Component {
49 setNetworkType(network)
49 setNetworkType(network)
50
50
51 const endpoint =
51 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}`
53
53
54 const provider = new Web3.providers.HttpProvider(endpoint)
54 const provider = new Web3.providers.HttpProvider(endpoint)
55 const web3 = new Web3(provider)
55 const web3 = new Web3(provider)
src/bridge/views/Point/Actions.js+2−2
@@ -215,7 +215,7 @@ const Actions = (props) => {
215 pushRoute(ROUTE_NAMES.SET_SPAWN_PROXY)
215 pushRoute(ROUTE_NAMES.SET_SPAWN_PROXY)
216 }}
216 }}
217 >
217 >
218− { 'Set spawn proxy' }
218+ { 'Change spawn proxy' }
219 </Button>
219 </Button>
220
220
221 <Button
221 <Button
@@ -226,7 +226,7 @@ const Actions = (props) => {
226 pushRoute(ROUTE_NAMES.SET_MANAGEMENT_PROXY)
226 pushRoute(ROUTE_NAMES.SET_MANAGEMENT_PROXY)
227 }}
227 }}
228 >
228 >
229− { 'Set management proxy' }
229+ { 'Change management proxy' }
230 </Button>
230 </Button>
231
231
232 <Button
232 <Button
src/bridge/views/SetProxy.js+58−29
@@ -1,7 +1,7 @@
1import Maybe from 'folktale/maybe'
1import Maybe from 'folktale/maybe'
2import React from 'react'
2import React from 'react'
3import * as azimuth from 'azimuth-js'
3import * 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'
5import { AddressInput } from '../components/Base'
5import { AddressInput } from '../components/Base'
6import * as ob from 'urbit-ob'
6import * as ob from 'urbit-ob'
7
7
@@ -46,13 +46,23 @@ class SetProxy extends React.Component {
46 this.state = {
46 this.state = {
47 proxyAddress: '',
47 proxyAddress: '',
48 issuingPoint: issuingPoint,
48 issuingPoint: issuingPoint,
49+ setProxy: true
49 }
50 }
50
51
51 this.statelessRef = React.createRef();
52 this.statelessRef = React.createRef();
53+ this.handleSetUnset = this.handleSetUnset.bind(this);
52 this.handleAddressInput = this.handleAddressInput.bind(this)
54 this.handleAddressInput = this.handleAddressInput.bind(this)
53 this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
55 this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
54 }
56 }
55
57
58+ handleSetUnset(selected) {
59+ const set = (selected === 'set');
60+ this.setState({
61+ setProxy: set,
62+ proxyAddress: set ? '' : '0x0000000000000000000000000000000000000000'
63+ });
64+ }
65+
56 handleAddressInput(proxyAddress) {
66 handleAddressInput(proxyAddress) {
57 this.setState({ proxyAddress })
67 this.setState({ proxyAddress })
58
68
@@ -116,42 +126,61 @@ class SetProxy extends React.Component {
116
126
117 const ucFirst = w => w.charAt(0).toUpperCase() + w.slice(1);
127 const ucFirst = w => w.charAt(0).toUpperCase() + w.slice(1);
118
128
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+ }
119
168
120 return (
169 return (
121 <Row>
170 <Row>
122 <Col>
171 <Col>
123 <H1>
172 <H1>
124− { `Set ${ucFirst(renderedProxyType)} Proxy For ` }
173+ { `${titleVerb} ${ucFirst(renderedProxyType)} Proxy For ` }
125 <code>{ `${ob.patp(state.issuingPoint)}` }</code>
174 <code>{ `${ob.patp(state.issuingPoint)}` }</code>
126 </H1>
175 </H1>
127
176
128− <P>
177+ <HorizontalSelector
129− {
178+ options={setUnset}
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) }>
179+ onChange={this.handleSetUnset}
143− <InnerLabel>{ 'Proxy Address' }</InnerLabel>
180+ className='mt-8'
144− <ShowBlockie className={'mt-1'} address={state.proxyAddress} />
181+ />
145− </AddressInput>
182+
146−
183+ { addressInput }
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>
155
184
156 <StatelessTransaction
185 <StatelessTransaction
157 // Upper scope
186 // Upper scope
src/bridge/views/Trezor.js+57−14
@@ -3,11 +3,11 @@ import React from 'react'
3import Maybe from 'folktale/maybe'
3import Maybe from 'folktale/maybe'
4import { Button } from '../components/Base'
4import { Button } from '../components/Base'
5import { Row, Col, H1, P } from '../components/Base'
5import { Row, Col, H1, P } from '../components/Base'
6−import { InnerLabel, Input } from '../components/Base'
6+import { InnerLabel, Input, InnerLabelDropdown } from '../components/Base'
7import TrezorConnect from 'trezor-connect'
7import TrezorConnect from 'trezor-connect'
8import * as secp256k1 from 'secp256k1'
8import * as secp256k1 from 'secp256k1'
9
9
10−import { TREZOR_BASE_PATH } from '../lib/trezor'
10+import { TREZOR_PATH } from '../lib/trezor'
11import { ROUTE_NAMES } from '../lib/router'
11import { ROUTE_NAMES } from '../lib/router'
12
12
13class Trezor extends React.Component {
13class Trezor extends React.Component {
@@ -16,13 +16,23 @@ class Trezor extends React.Component {
16 super(props)
16 super(props)
17
17
18 this.state = {
18 this.state = {
19− hdpath: TREZOR_BASE_PATH
19+ hdpath: TREZOR_PATH.replace(/x/g, 0),
20+ account: 0
20 }
21 }
21
22
23+ this.handleAccountSelection = this.handleAccountSelection.bind(this)
22 this.handleHdPathInput = this.handleHdPathInput.bind(this)
24 this.handleHdPathInput = this.handleHdPathInput.bind(this)
23 this.pollDevice = this.pollDevice.bind(this)
25 this.pollDevice = this.pollDevice.bind(this)
24 }
26 }
25
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+
26 handleHdPathInput(hdpath) {
36 handleHdPathInput(hdpath) {
27 this.setState({ hdpath })
37 this.setState({ hdpath })
28 }
38 }
@@ -53,7 +63,48 @@ class Trezor extends React.Component {
53
63
54 render() {
64 render() {
55 const { pushRoute, popRoute, wallet } = this.props
65 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+ );
57
108
58 return (
109 return (
59 <Row>
110 <Row>
@@ -66,16 +117,8 @@ class Trezor extends React.Component {
66 }
117 }
67 </P>
118 </P>
68
119
69− <Input
120+ { accountSelection }
70− className='pt-8 text-mono'
121+ { pathSelection }
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>
79
122
80 <Button
123 <Button
81 className={'mt-8'}
124 className={'mt-8'}