on master help sign in
Merged
commit32a3c9952b36a35f5d2f0d099490224cd91c734e authorChris Johnson <chris@cjohnson.io> dateThu 25 Apr 2019, 14:35 (8 years ago) parent6180f5e7029bf4
30 files changed+1086−397
CHANGELOG+17−1 README.md+44−19 bridge-https.py−1 package.json+9−9 src/Root.js−5 src/bridge/Bridge.js+18−3 src/bridge/components/InnerLabelDropdown.js+88−45 src/bridge/components/ReactSVGComponents.js+24−24 src/bridge/components/StatelessTransaction.js+99−23 src/bridge/lib/contracts.js+4−1 src/bridge/lib/inviteMail.js+27 src/bridge/lib/router.js+13 src/bridge/lib/txn.js+6−6 src/bridge/lib/wallet.js+2−2 src/bridge/style/extend.css+8 src/bridge/views/AcceptTransfer.js+1 src/bridge/views/CancelTransfer.js+1 src/bridge/views/CreateGalaxy.js+2−1 src/bridge/views/InvitesManage.js+207 src/bridge/views/InvitesSend.js+262 src/bridge/views/IssueChild.js+2−1 src/bridge/views/Network.js+47−99 src/bridge/views/Point.js+5−5 src/bridge/views/Point/Actions.js+56−23 src/bridge/views/Points.js+5−1 src/bridge/views/SentTransaction.js+104−44 src/bridge/views/SetKeys.js+1 src/bridge/views/SetProxy.js+1 src/bridge/views/Transfer.js+1 src/bridge/views/Wallet.js+32−84
CHANGELOG+17−1
@@ -1,6 +1,22 @@
11
22# Changelog
33
4+- 1.3.1 (2019-04-23)
5+ This release fixes an issue with the script for serving Bridge over HTTPS.
6+
7+ * Stop `cd`ing into the new-removed `build` directory (#123)
8+
9+- 1.3.0 (2019-04-18)
10+ This release simplifies the flows surrounding networking keys and brings many other quality of life improvements.
11+
12+ * Simplify the network key generation & download flows (#85, #92)
13+ * Allow viewing points without logging in (#80)
14+ * Display transaction confirmation status after submission (#99)
15+ * Support the optional BIP39 mnemonic passphrase (#94)
16+ * Makes the browser back button work for navigating in Bridge (#86)
17+ * Various smaller UI improvements (#87, #100, #104, #106, #113, #116)
18+ * Various smaller code improvements (#82, #91, #95, #96, #108, #115)
19+
420- 1.2.1 (2019-03-21)
521 This release fixes an issue with the spawning of child points.
622
@@ -29,4 +45,4 @@
2945
3046 * Add option for setting Trezor and Ledger custom derivation path
3147 (instead of defaulting to 0 index address)
32− * Fixes Trezor transaction signing bug
48+ * Fixes Trezor transaction signing bug
README.md+44−19
@@ -4,9 +4,49 @@ An application for interacting with Azimuth.
44
55## Usage
66
7−To run Bridge, follow the instructions on the [latest release][rele] page.
7+### Requirements
88
9−## Development Notes
9+Python 3.7.2
10+
11+### Instructions
12+
13+#### Install
14+
15+1. [Download a release](https://github.com/urbit/bridge/releases)
16+2. Unzip it (`bridge-$version.zip`)
17+3. Open up your command line interface (Terminal on MacOS, Command Prompt on Windows)
18+4. `cd` into the `bridge-$version` directory
19+
20+Follow the instructions below to run Bridge.
21+
22+#### Run Bridge
23+
24+If you plan to authenticate and sign transactions with a Master Ticket, BIP39 mnemonic, Ethereum private key, or keystore file:
25+
26+1. `cd` into the `bridge-$version` directory
27+2. Run `python3 -m http.server 5000 --bind 127.0.0.1`
28+3. Navigate to http://localhost:5000 using a web browser to access Bridge (we recommend using Firefox or Chrome)
29+
30+#### Run Bridge with Ledger support
31+
32+If you plan to authenticate and sign transactions with a [Ledger](https://www.ledger.com/), Bridge must be serving over HTTPS on localhost. This requires self-signed certificates. To do this:
33+
34+1. Install [mkcert](https://github.com/FiloSottile/mkcert)
35+2. If you're using Firefox, additionally install [nss](https://github.com/nss-dev/nss)
36+3. Install a local certificate authority via `mkcert -install`
37+4. From the `bridge-$version` directory, run `mkcert localhost` to generate a certificate valid for localhost. This will produce two files: `localhost.pem`, the local certificate, and `localhost-key.pem`, its corresponding private key
38+5. Run `python bridge-https.py`
39+6. Navigate to https://localhost:4443 in a web browser to access Bridge
40+
41+### Verify checksums
42+
43+To validate your downloaded file's integrity, compare the lines in checksum.txt to SHA-256 hashes of the `bridge-$version` directory's contents.
44+
45+- On MacOS: `shasum -a 256 -c checksums.txt .`
46+- On Linux: `sha256sum -c checksums.txt .`
47+- On Windows: Go into the `build` directory and verify files individually with `CertUtil -hashFile [file_name] SHA256`
48+
49+## Development notes
1050
1151### Install / Build
1252
@@ -32,22 +72,7 @@ $ nvm use v11.0.0
3272
3373before running `npm run pilot`.
3474
35−### Ledger support
36−
37−To authenticate and sign transactions with a Ledger, Bridge must be serving
38−over HTTPS on localhost. This requires self-signed certificates. You can do
39−this via the following:
40−
41−* Install [mkcert](https://github.com/FiloSottile/mkcert)
42−* Install a local certificate authority via `mkcert -install`
43−* In your `bridge` directory, generate a certificate valid for localhost via
44− `mkcert localhost`. This will produce two files: `localhost.pem`, the local
45− certificate, and `localhost-key.pem`, its corresponding private key.
46−* Run `python bridge-https.py`
47−
48−Bridge will serve to localhost over HTTPS on port 4443
49−
50−### Useful Accounts
75+### Useful accounts
5176
5277The ecliptic owner is the only account that's able to create galaxies, so
5378it's a good place to get started. On the testnet, it's the address:
@@ -81,7 +106,7 @@ with 100 ETH:
81106To play around with any of these, authenticate using the same mnemonic, but
82107use a custom HD path of `m/44'/60'/0'/0/1`, `m/44'/60'/0'/0/2`, and so on.
83108
84−### Initial Development State
109+### Initial development state
85110
86111You can also tweak a couple of things to change your development state
87112somewhat (say, for example, you want to start on the points list screen, instead
bridge-https.py−1
@@ -4,5 +4,4 @@ import ssl
44
55httpd = BaseHTTPServer.HTTPServer(('localhost', 4443), SimpleHTTPServer.SimpleHTTPRequestHandler)
66httpd.socket = ssl.wrap_socket (httpd.socket, certfile='localhost.pem', keyfile='localhost-key.pem', server_side=True)
7−os.chdir('./build')
87httpd.serve_forever()
package.json+9−9
@@ -1,6 +1,6 @@
11{
22 "name": "bridge",
3− "version": "1.2.1",
3+ "version": "1.3.1",
44 "description": "An application for interacting with Azimuth",
55 "repository": {
66 "type": "git",
@@ -14,10 +14,11 @@
1414 "node": "11.0.0"
1515 },
1616 "dependencies": {
17− "azimuth-js": "^0.10.0",
1817 "@ledgerhq/hw-app-eth": "^4.35.0",
1918 "@ledgerhq/hw-transport-u2f": "^4.35.0",
20− "azimuth-solidity": "1.0.2",
19+ "PaperCollateralRenderer": "github:urbit/PaperCollateralRenderer",
20+ "azimuth-js": "^0.13.2",
21+ "azimuth-solidity": "^1.1.0",
2122 "babel-polyfill": "^6.26.0",
2223 "bip32": "^1.0.2",
2324 "bip39": "^2.5.0",
@@ -34,7 +35,6 @@
3435 "keythereum": "^1.0.4",
3536 "lodash": "^4.17.11",
3637 "more-entropy": "^0.0.7",
37− "PaperCollateralRenderer": "github:urbit/PaperCollateralRenderer",
3838 "prop-types": "^15.6.2",
3939 "react": "^16.6.0",
4040 "react-dom": "^16.6.0",
@@ -45,18 +45,18 @@
4545 "trezor-connect": "^7.0.1",
4646 "urbit-key-generation": "0.12.1",
4747 "urbit-ob": "^3.1.0",
48− "web3": "1.0.0-beta.33"
48+ "web3": "1.0.0-beta.33",
49+ "web3-utils": "^1.0.0-beta.52"
4950 },
5051 "scripts": {
5152 "start": "react-scripts start",
5253 "build": "react-scripts build",
5354 "test": "react-scripts test --env=jsdom",
5455 "eject": "react-scripts eject",
55− "checksum": "find build -exec shasum -a256 {} ';' > checksums.txt",
56− "tar": "tar -czvf bridge-$npm_package_version.tar.gz README.md bridge-https.py checksums.txt build",
57− "zip": "zip -FSr bridge-$npm_package_version.zip README.md bridge-https.py checksums.txt build",
56+ "checksum": "cd build && find . -exec shasum -a256 {} ';' > ../checksums.txt && cd ..",
57+ "zip": "zip -ur bridge-$npm_package_version.zip README.md bridge-https.py checksums.txt && cd build && zip -ur ../bridge-$npm_package_version.zip ./* && cd ..",
5858 "release": "npm install && npm run build && npm run checksum && npm run zip",
59− "pilot:ganache": "ganache-cli -m 'benefit crew supreme gesture quantum web media hazard theory mercy wing kitten' > /dev/null &",
59+ "pilot:ganache": "ganache-cli --blockTime 1 -m 'benefit crew supreme gesture quantum web media hazard theory mercy wing kitten' > /dev/null &",
6060 "pilot:deploy": "cd node_modules/azimuth-solidity && truffle deploy",
6161 "pilot:setup": "npm run pilot:ganache && npm run pilot:deploy",
6262 "pilot": "HTTPS=true npm-run-all pilot:setup start pilot:cleanup --continue-on-error",
src/Root.js−5
@@ -9,7 +9,6 @@ import { AppNavigation } from './common/components/AppNavigation'
99
1010import './common/style/index.css'
1111import './bridge/style/index.css'
12−import './walletgen/style/index.css'
1312
1413class Root extends React.Component {
1514 constructor(props) {
@@ -88,10 +87,6 @@ class Root extends React.Component {
8887 return (
8988 <React.Fragment>
9089 {appComponent}
91− <AppNavigation
92− setView={this.setView}
93− appView={this.state.appView}
94− />
9590 </React.Fragment>
9691 )
9792 }
src/bridge/Bridge.js+18−3
@@ -25,7 +25,11 @@ const initWeb3 = (networkType) => {
2525 const contracts = azimuth.initContracts(web3, CONTRACT_ADDRESSES.MAINNET)
2626 return {web3: web3, contracts: contracts}
2727 } else if (networkType === NETWORK_NAMES.LOCAL) {
28− const endpoint = 'ws://localhost:8545'
28+ const protocol =
29+ process.env.NODE_ENV === 'development'
30+ ? 'ws'
31+ : 'wss'
32+ const endpoint = `${protocol}://localhost:8545`
2933 const provider = new Web3.providers.WebsocketProvider(endpoint)
3034 const web3 = new Web3(provider)
3135 const contracts = azimuth.initContracts(web3, CONTRACT_ADDRESSES.DEV)
@@ -80,7 +84,8 @@ class Bridge extends React.Component {
8084 pointCursor: Nothing(),
8185 pointCache: {},
8286 // txn
83− txnHashCursor: Nothing()
87+ txnHashCursor: Nothing(),
88+ txnConfirmations: {}
8489 }
8590
8691 this.pushRoute = this.pushRoute.bind(this)
@@ -95,6 +100,7 @@ class Bridge extends React.Component {
95100 this.setAuthMnemonic = this.setAuthMnemonic.bind(this)
96101 this.setPointCursor = this.setPointCursor.bind(this)
97102 this.setTxnHashCursor = this.setTxnHashCursor.bind(this)
103+ this.setTxnConfirmations = this.setTxnConfirmations.bind(this)
98104 this.setNetworkSeedCache = this.setNetworkSeedCache.bind(this)
99105 this.addToPointCache = this.addToPointCache.bind(this)
100106 }
@@ -244,6 +250,12 @@ class Bridge extends React.Component {
244250 this.setState({ txnHashCursor })
245251 }
246252
253+ setTxnConfirmations(txnHash, txnConfirmations) {
254+ this.setState((prevState, _) =>
255+ ({txnConfirmations: { ...prevState.txnConfirmations,
256+ [txnHash]: txnConfirmations}}))
257+ }
258+
247259 render() {
248260 const {
249261 routeCrumbs,
@@ -258,6 +270,7 @@ class Bridge extends React.Component {
258270 pointCursor,
259271 pointCache,
260272 txnHashCursor,
273+ txnConfirmations,
261274 web3,
262275 contracts
263276 } = this.state
@@ -319,7 +332,9 @@ class Bridge extends React.Component {
319332 setNetworkSeedCache= { this.setNetworkSeedCache }
320333 // txn
321334 setTxnHashCursor={ this.setTxnHashCursor }
322− txnHashCursor={ txnHashCursor } />
335+ txnHashCursor={ txnHashCursor }
336+ setTxnConfirmations={ this.setTxnConfirmations }
337+ txnConfirmations={ txnConfirmations } />
323338
324339 <div className={'push'} />
325340
src/bridge/components/InnerLabelDropdown.js+88−45
@@ -1,71 +1,114 @@
11import React from 'react'
22import PropTypes from 'prop-types';
33
4+import { DropdownItem, DropdownDivider } from '../components/Base'
45
5−const InnerLabelDropdown = props => {
6−
7− const menuVisibility = props.isOpen === true
8− ? 'visible'
9− : 'hidden';
10−
11− const selectedClasses = props.isOpen === true
12− ? 'br-blue'
13− : 'br-gray-50';
14−
15− return (
16− <div className={'rel table'}>
17− <div
18− className={`fix full o-0 ${menuVisibility}`}
19− onClick={props.handleClose}
20− />
21−
22− <button
23− className={`h-10 mp-0 bg-white outline-blue sans ${props.className}`}
24− onClick={props.handleToggle}
25− disabled={props.disabled}>
26− <div className={'flex'}>
27− <div className={`ph-4 flex-center-all b-2 br-0 b-solid h-10 ${selectedClasses}`}>
28− <div className={'text-400 mr-2'}>{props.title}</div>
29− <div className={'text-600'}>{props.currentSelectionTitle}</div>
6+class InnerLabelDropdown extends React.Component {
7+ constructor(props) {
8+ super(props)
9+
10+ this.state = {
11+ isOpen: false
12+ }
13+
14+ this.toggle = this.toggle.bind(this)
15+ this.close = this.close.bind(this)
16+ }
17+
18+ toggle() {
19+ this.setState({
20+ isOpen: !this.state.isOpen
21+ })
22+ }
23+
24+ close() {
25+ this.setState({
26+ isOpen: false
27+ })
28+ }
29+
30+ selectOption(opt) {
31+ this.close()
32+ this.props.handleUpdate(opt)
33+ }
34+
35+ getOptionElems() {
36+ return this.props.options.map(opt => {
37+ if (opt.type === "divider") {
38+ return <DropdownDivider />
39+ } else {
40+ return (
41+ <DropdownItem onClick={() => this.selectOption(opt.value)}>
42+ { opt.title }
43+ </DropdownItem>
44+ )
45+ }
46+ })
47+ }
48+
49+ render() {
50+ const { props, state } = this
51+
52+ const optionElems = this.getOptionElems()
53+
54+ const menuVisibility = state.isOpen === true
55+ ? 'visible'
56+ : 'hidden';
57+
58+ const selectedClasses = state.isOpen === true
59+ ? 'br-blue'
60+ : 'br-gray-50';
61+
62+ const fullWidthClass = props.fullWidth ? 'full-width' : ''
63+
64+ return (
65+ <div className={`rel table ${fullWidthClass}`}>
66+ <div
67+ className={`fix full o-0 ${menuVisibility}`}
68+ onClick={this.close}
69+ />
70+
71+ <button
72+ className={`h-10 mp-0 bg-white outline-blue sans ${props.className} ${fullWidthClass}`}
73+ onClick={this.toggle}
74+ disabled={props.disabled}>
75+ <div className={'flex'}>
76+ <div className={`ph-4 flex align-center b-2 br-0 b-solid h-10 ${selectedClasses} ${fullWidthClass}`}>
77+ <div className={'text-400 mr-2'}>{props.title}</div>
78+ <div className={'text-600'}>{props.currentSelectionTitle}</div>
79+ </div>
80+ <div className={'white bg-blue s-10 flex-center-all text-sm'}>{'▼'}</div>
3081 </div>
31− <div className={'white bg-blue s-10 flex-center-all text-sm'}>{'▼'}</div>
32− </div>
33− </button>
82+ </button>
3483
35− <div className={`abs flex-column bg-white t-12 l-0 w-100 ${menuVisibility}`}>
36− <div className={'pv-1 b-solid b-gray-50 b-2 bb-0'} />
84+ <div className={`abs flex-column bg-white t-12 l-0 w-100 z-1 ${menuVisibility}`}>
85+ <div className={'pv-1 b-solid b-gray-50 b-2 bb-0'} />
3786
38− { props.children }
87+ { optionElems }
3988
40− <div className={'pv-1 b-solid b-gray-50 b-2 bt-0'} />
41− </div>
89+ <div className={'pv-1 b-solid b-gray-50 b-2 bt-0'} />
90+ </div>
4291
43− </div>
44− )
92+ </div>
93+ )
94+ }
4595}
4696
4797InnerLabelDropdown.propTypes = {
4898 disabled: PropTypes.bool,
49− isOpen: PropTypes.bool,
50− handleToggle: PropTypes.func,
51− handleClose: PropTypes.func,
5299 className: PropTypes.string,
53100 title: PropTypes.string,
54− children: PropTypes.node,
55101 style: PropTypes.object,
56102 currentSelectionTitle: PropTypes.string,
103+ children: PropTypes.node,
57104};
58105
59−
60106InnerLabelDropdown.defaultProps = {
61− isOpen: false,
107+ disabled: false,
62108 className: '',
63109 title: '',
64− currentSelectionTitle: '',
65− handleToggle: () => {},
66− handleClose: () => {},
67− disabled: false,
68110 style: {},
111+ currentSelectionTitle: '',
69112};
70113
71114export default InnerLabelDropdown
src/bridge/components/ReactSVGComponents.js+24−24
@@ -2,59 +2,59 @@ import React from 'react'
22import { get, map } from 'lodash'
33
44const ReactSVGComponents = {
5− svg: p => {
5+ svg: (p, key) => {
66 return (
7− <svg {...p.attr} version={'1.1'} xmlns={'http://www.w3.org/2000/svg'}>
8− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
7+ <svg {...p.attr} version={'1.1'} xmlns={'http://www.w3.org/2000/svg'} key={key}>
8+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
99 </svg>
1010 )
1111 },
12− circle: p => {
12+ circle: (p, key) => {
1313 return (
14− <circle {...p.attr}>
15− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
14+ <circle {...p.attr} key={key}>
15+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
1616 </circle>
1717 )
1818 },
19− rect: p => {
19+ rect: (p, key) => {
2020 return (
21− <rect {...p.attr}>
22− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
21+ <rect {...p.attr} key={key}>
22+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
2323 </rect>
2424 )
2525 },
26− path: p => {
26+ path: (p, key) => {
2727 return (
28− <path {...p.attr}>
29− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
28+ <path {...p.attr} key={key}>
29+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
3030 </path>
3131 )
3232 },
33− g: p => {
33+ g: (p, key) => {
3434 return (
35− <g {...p.attr}>
36− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
35+ <g {...p.attr} key={key}>
36+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
3737 </g>
3838 )
3939 },
40− polygon: p => {
40+ polygon: (p, key) => {
4141 return (
42− <polygon {...p.attr}>
43− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
42+ <polygon {...p.attr} key={key}>
43+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
4444 </polygon>
4545 )
4646 },
47− line: p => {
47+ line: (p, key) => {
4848 return (
49− <line {...p.attr}>
50− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
49+ <line {...p.attr} key={key}>
50+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
5151 </line>
5252 )
5353 },
54− polyline: p => {
54+ polyline: (p, key) => {
5555 return (
56− <polyline {...p.attr}>
57− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
56+ <polyline {...p.attr} key={key}>
57+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
5858 </polyline>
5959 )
6060 }
src/bridge/components/StatelessTransaction.js+99−23
@@ -3,7 +3,8 @@ import React from 'react'
33
44import { Code, H3 } from './Base'
55import { Button } from './Base'
6−import { CheckboxButton, Input, InnerLabel } from './Base'
6+import { CheckboxButton, Input, InnerLabel,
7+ InnerLabelDropdown } from './Base'
78import { Warning } from '../components/Base'
89
910import { addressFromSecp256k1Public } from '../lib/wallet'
@@ -27,11 +28,12 @@ class StatelessTransaction extends React.Component {
2728 gasLimit: '600000',
2829 showGasDetails: false,
2930 userApproval: false,
30− chainId: '',
31− nonce: '',
31+ chainId: '1',
32+ customChain: false,
33+ nonce: '0',
3234 stx: Nothing(),
3335 txn: Nothing(),
34− txError: Nothing(),
36+ txError: Nothing()
3537 }
3638
3739 this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
@@ -46,6 +48,7 @@ class StatelessTransaction extends React.Component {
4648 this.setGasLimit = this.setGasLimit.bind(this)
4749 this.rangeChange = this.rangeChange.bind(this)
4850 this.toggleGasDetails = this.toggleGasDetails.bind(this)
51+ this.handleChainUpdate = this.handleChainUpdate.bind(this)
4952 }
5053
5154 componentDidMount() {
@@ -85,17 +88,20 @@ class StatelessTransaction extends React.Component {
8588
8689 submit(){
8790 const { props, state } = this
88− sendSignedTransaction(props.web3.value, state.stx)
91+ sendSignedTransaction(props.web3.value, state.stx, props.setTxnConfirmations)
8992 .then(sent => {
9093 props.setTxnHashCursor(sent)
9194 props.popRoute()
9295
96+ let routeData = {};
9397 if (props.networkSeed) {
94− props.setNetworkSeedCache(props.networkSeed)
95− props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION, {promptKeyfile: true})
96− } else {
97− props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION)
98+ props.setNetworkSeedCache(props.networkSeed);
99+ routeData.promptKeyfile = true;
100+ }
101+ if (props.promptText) {
102+ routeData.promptText = props.promptText;
98103 }
104+ props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION, routeData);
99105 })
100106 .catch(err => {
101107 if (err.map) {
@@ -106,6 +112,21 @@ class StatelessTransaction extends React.Component {
106112 })
107113 }
108114
115+ handleChainUpdate(chainId) {
116+ if (chainId === "custom") {
117+ this.setState({
118+ customChain: true
119+ })
120+ } else {
121+ this.setState({
122+ customChain: false,
123+ chainId
124+ })
125+ }
126+
127+ this.clearStx()
128+ }
129+
109130 setUserApproval(){
110131 const {state} = this
111132 this.setState({ userApproval: !state.userApproval })
@@ -185,13 +206,53 @@ class StatelessTransaction extends React.Component {
185206 this.submit()
186207 }
187208
209+ getChainTitle(chainId) {
210+ let map = {
211+ "1": "Mainnet - 1",
212+ "2": "Morden - 2",
213+ "3": "Ropsten - 3",
214+ "4": "Goerli - 4",
215+ "42": "Kovan - 42",
216+ "1337": "Geth private chains - 1337",
217+ "custom": "Custom"
218+ }
219+
220+ return map[chainId]
221+ }
222+
223+ getChainOptions() {
224+ return [{
225+ title: this.getChainTitle("1"),
226+ value: "1"
227+ }, {
228+ title: this.getChainTitle("2"),
229+ value: "2"
230+ }, {
231+ title: this.getChainTitle("3"),
232+ value: "3"
233+ }, {
234+ title: this.getChainTitle("4"),
235+ value: "4"
236+ }, {
237+ title: this.getChainTitle("42"),
238+ value: "42"
239+ }, {
240+ title: this.getChainTitle("1337"),
241+ value: "1337"
242+ }, {
243+ title: this.getChainTitle("custom"),
244+ value: "custom"
245+ }]
246+ }
247+
188248 render() {
189249 const { web3, canGenerate } = this.props
190250 const { gasPrice, gasLimit, nonce, chainId,
191− txn, stx, userApproval, showGasDetails } = this.state
251+ txn, stx, userApproval, showGasDetails,
252+ customChain } = this.state
192253
193254 const { setNonce, setChainId, setGasLimit, setGasPrice, toggleGasDetails,
194− setUserApproval, sendTxn, createUnsignedTxn } = this
255+ setUserApproval, sendTxn, createUnsignedTxn, handleChainUpdate } = this
195256
196257 const { state } = this
197258
@@ -199,6 +260,8 @@ class StatelessTransaction extends React.Component {
199260 const canApprove = Just.hasInstance(stx)
200261 const canSend = Just.hasInstance(stx) && userApproval === true
201262
263+ const chainOptions = this.getChainOptions()
264+
202265 const generateButtonColor =
203266 Nothing.hasInstance(txn)
204267 ? 'blue'
@@ -294,7 +357,7 @@ class StatelessTransaction extends React.Component {
294357
295358 const nonceDialogue =
296359 <Input
297− className={ 'mono mt-4' }
360+ className={ 'mono mt-4 mb-4' }
298361 prop-size={ 'md' }
299362 prop-format={ 'innerLabel' }
300363 value={ nonce }
@@ -305,18 +368,30 @@ class StatelessTransaction extends React.Component {
305368 </InnerLabel>
306369 </Input>
307370
371+ const chainDialogueTitle = customChain ? "Custom" : this.getChainTitle(chainId)
308372 const chainDialogue =
309− <Input
310− className={ 'mono mt-4 mb-8' }
311− prop-size={ 'md' }
312− prop-format={ 'innerLabel' }
313− value={ chainId }
314− onChange={ setChainId }
373+ <InnerLabelDropdown
374+ className={'mt-6'}
375+ fullWidth={true}
376+ title={'Chain ID'}
377+ options={chainOptions}
378+ handleUpdate={handleChainUpdate}
379+ currentSelectionTitle={chainDialogueTitle}
315380 >
316− <InnerLabel>
317− { 'Chain ID' }
318− </InnerLabel>
319− </Input>
381+ </InnerLabelDropdown>
382+
383+ const customChainDialogue = !customChain ? null :
384+ <Input
385+ className={ 'mono mt-4 mb-8' }
386+ prop-size={ 'md' }
387+ prop-format={ 'innerLabel' }
388+ value={ chainId }
389+ onChange={ setChainId }
390+ >
391+ <InnerLabel>
392+ { 'Chain ID' }
393+ </InnerLabel>
394+ </Input>
320395
321396 const onlineParamsDialogue = web3.matchWith({
322397 Just: _ => <div />,
@@ -324,6 +399,7 @@ class StatelessTransaction extends React.Component {
324399 <React.Fragment>
325400 { nonceDialogue }
326401 { chainDialogue }
402+ { customChainDialogue }
327403 </React.Fragment>
328404 })
329405
@@ -358,7 +434,7 @@ class StatelessTransaction extends React.Component {
358434 onClick={ setUserApproval }
359435 state={ userApproval }
360436 >
361− <div>
437+ <div onClick={ setUserApproval }>
362438 { `I approve this transaction and wish to send.` }
363439 </div>
364440 </CheckboxButton>
src/bridge/lib/contracts.js+4−1
@@ -3,17 +3,20 @@ const CONTRACT_ADDRESSES = {
33 DEV: {
44 ecliptic: '0x56db68f29203ff44a803faa2404a44ecbb7a7480',
55 azimuth: '0x863d9c2e5c4c133596cfac29d55255f0d0f86381',
6− polls: '0x935452c45eda2958976a429c9733c40302995efd'
6+ polls: '0x935452c45eda2958976a429c9733c40302995efd',
7+ delegatedSending: '0xb71c0b6cee1bcae56dfe95cd9d3e41ddd7eafc43'
78 },
89 ROPSTEN: {
910 ecliptic: '0x8b9f86a28921d9c705b3113a755fb979e1bd1bce',
1011 azimuth: '0x308ab6a6024cf198b57e008d0ac9ad0219886579',
1112 polls: '0xf5DA85f0d285A0F88af2388DD177A221872C8971'
13+ //TODO delegatedSending
1214 },
1315 MAINNET: {
1416 ecliptic: '0x6ac07b7c4601b5ce11de8dfe6335b871c7c4dd4d',
1517 azimuth: '0x223c067f8cf28ae173ee5cafea60ca44c335fecb',
1618 polls: '0x7fecab617c868bb5996d99d95200d2fa708218e4'
19+ //TODO delegatedSending
1720 },
1821}
1922
src/bridge/lib/inviteMail.jsnew file+27
@@ -0,0 +1,27 @@
1+//NOTE if accessing this in a localhost configuration fails with "CORS request
2+// did not succeed", you might need to visit localhost:3001 or whatever
3+// explicitly and tell your browser that's safe to access.
4+// https://stackoverflow.com/a/53011185/1334324
5+const baseUrl = 'https://localhost:3002'
6+
7+function sendMail(recipient, ticket) {
8+ return new Promise((resolve, reject) => {
9+ fetch(baseUrl + '/send-ticket', {
10+ method: "POST",
11+ // mode: "cors", // no-cors, cors, *same-origin
12+ cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
13+ // credentials: "omit", // include, *same-origin, omit
14+ headers: {
15+ "Content-Type": "application/json",
16+ },
17+ body: JSON.stringify({recipient,ticket}),
18+ })
19+ .then(response => resolve(response.ok))
20+ .catch(reject);
21+ });
22+}
23+
24+export {
25+ sendMail
26+}
27+
src/bridge/lib/router.js+13
@@ -7,6 +7,9 @@ import Landing from '../views/Landing'
77import InviteTicket from '../views/InviteTicket'
88import InviteTransactions from '../views/InviteTransactions.js'
99
10+import InvitesSend from '../views/InvitesSend.js'
11+import InvitesManage from '../views/InvitesManage.js'
12+
1013import AcceptTransfer from '../views/AcceptTransfer'
1114import CancelTransfer from '../views/CancelTransfer'
1215import CreateGalaxy from '../views/CreateGalaxy'
@@ -42,6 +45,8 @@ const ROUTE_NAMES = {
4245 //
4346 INVITE_TICKET: Symbol('INVITE_TICKET'),
4447 INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
48+ INVITES_SEND: Symbol('INVITES_SEND'),
49+ INVITES_MANAGE: Symbol('INVITES_MANAGE'),
4550 //
4651 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
4752 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
@@ -75,6 +80,8 @@ const createRoutes = () => {
7580 //
7681 routes[ROUTE_NAMES.INVITE_TICKET] = InviteTicket
7782 routes[ROUTE_NAMES.INVITE_TRANSACTIONS] = InviteTransactions
83+ routes[ROUTE_NAMES.INVITES_SEND] = InvitesSend
84+ routes[ROUTE_NAMES.INVITES_MANAGE] = InvitesManage
7885 //
7986 routes[ROUTE_NAMES.ACCEPT_TRANSFER] = AcceptTransfer
8087 routes[ROUTE_NAMES.CANCEL_TRANSFER] = CancelTransfer
@@ -117,6 +124,12 @@ const renderRoute = (props, route) => {
117124 : route === ROUTE_NAMES.INVITE_TRANSACTIONS
118125 ? 'Setting up new wallet'
119126
127+ : route === ROUTE_NAMES.INVITES_SEND
128+ ? 'Send invites'
129+
130+ : route === ROUTE_NAMES.INVITES_MANAGE
131+ ? 'Manage invites'
132+
120133 : route === ROUTE_NAMES.NETWORK
121134 ? `${renderNetworkType(networkType)}`
122135
src/bridge/lib/txn.js+6−6
@@ -3,6 +3,7 @@ import { Just } from 'folktale/maybe'
33import { Ok, Error } from 'folktale/result'
44import Tx from 'ethereumjs-tx'
55import Web3 from 'web3'
6+import { toWei, fromWei, toHex } from 'web3-utils'
67
78import { BRIDGE_ERROR } from '../lib/error'
89import { NETWORK_NAMES } from '../lib/network'
@@ -137,7 +138,7 @@ const signTransaction = async config => {
137138 setStx(Just(stx))
138139}
139140
140−const sendSignedTransaction = (web3, stx) => {
141+const sendSignedTransaction = (web3, stx, confirmationCb) => {
141142 const txn = stx.matchWith({
142143 Just: (tx) => tx.value,
143144 Nothing: () => {
@@ -155,6 +156,10 @@ const sendSignedTransaction = (web3, stx) => {
155156 .on('receipt', txn => {
156157 resolve(Just(Ok(txn.transactionHash)))
157158 })
159+ .on('confirmation', (confirmationNum, txn) => {
160+ confirmationCb(txn.transactionHash, confirmationNum + 1)
161+ resolve(Just(Ok(txn.transactionHash)))
162+ })
158163 .on('error', err => {
159164 reject(Just(Error(err.message)))
160165 })
@@ -203,11 +208,6 @@ const getTxnInfo = async (web3, addr) => {
203208 }
204209}
205210
206−const dummy = new Web3()
207−const toHex = dummy.utils.toHex
208−const toWei = dummy.utils.toWei
209−const fromWei = dummy.utils.fromWei
210−
211211const canDecodePatp = p => {
212212 try {
213213 ob.patp2dec(p)
src/bridge/lib/wallet.js+2−2
@@ -6,6 +6,7 @@ import Maybe from 'folktale/maybe'
66import * as secp256k1 from 'secp256k1'
77import * as ob from 'urbit-ob'
88import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
9+import { isAddress } from 'web3-utils'
910
1011const DEFAULT_HD_PATH = "m/44'/60'/0'/0/0"
1112
@@ -71,8 +72,7 @@ const stripHexPrefix = hex =>
7172const keccak256 = str =>
7273 keccak('keccak256').update(str).digest()
7374
74−const isValidAddress = (address) =>
75− /^0x[0-9a-fA-F]{40}$/.test(address)
75+const isValidAddress = isAddress
7676
7777const toChecksumAddress = (address) => {
7878 const addr = stripHexPrefix(address).toLowerCase()
src/bridge/style/extend.css+8
@@ -494,6 +494,14 @@ code {
494494 flex-wrap: wrap;
495495}
496496
497+.full-width {
498+ width: 100%;
499+}
500+
501+.z-1 {
502+ z-index: 1
503+}
504+
497505/**
498506 Passport styles
499507**/
src/bridge/views/AcceptTransfer.js+1
@@ -126,6 +126,7 @@ class AcceptTransfer extends React.Component {
126126 walletHdPath={props.walletHdPath}
127127 networkType={props.networkType}
128128 setTxnHashCursor={props.setTxnHashCursor}
129+ setTxnConfirmations={props.setTxnConfirmations}
129130 popRoute={props.popRoute}
130131 pushRoute={props.pushRoute}
131132 // Other
src/bridge/views/CancelTransfer.js+1
@@ -87,6 +87,7 @@ class CancelTransfer extends React.Component {
8787 walletHdPath={props.walletHdPath}
8888 networkType={props.networkType}
8989 setTxnHashCursor={props.setTxnHashCursor}
90+ setTxnConfirmations={props.setTxnConfirmations}
9091 popRoute={props.popRoute}
9192 pushRoute={props.pushRoute}
9293 // Other
src/bridge/views/CreateGalaxy.js+2−1
@@ -167,7 +167,7 @@ class CreateGalaxy extends React.Component {
167167 <ValidatedSigil
168168 className='tr-0 mt-05 mr-0 abs'
169169 patp={state.galaxyName}
170− size={76}
170+ size={68}
171171 margin={8} />
172172 </GalaxyInput>
173173
@@ -212,6 +212,7 @@ class CreateGalaxy extends React.Component {
212212 walletHdPath={props.walletHdPath}
213213 networkType={props.networkType}
214214 setTxnHashCursor={props.setTxnHashCursor}
215+ setTxnConfirmations={props.setTxnConfirmations}
215216 popRoute={props.popRoute}
216217 pushRoute={props.pushRoute}
217218 // Other
src/bridge/views/InvitesManage.jsnew file+207
@@ -0,0 +1,207 @@
1+import { Just, Nothing } from 'folktale/maybe'
2+import React from 'react'
3+import { pour } from 'sigil-js'
4+import * as ob from 'urbit-ob'
5+import * as azimuth from 'azimuth-js'
6+
7+import {
8+ EthereumWallet,
9+ addressFromSecp256k1Public,
10+ urbitWalletFromTicket,
11+ addHexPrefix,
12+ WALLET_NAMES
13+} from '../lib/wallet'
14+
15+import ReactSVGComponents from '../components/ReactSVGComponents'
16+import KeysAndMetadata from './Point/KeysAndMetadata'
17+import Actions from './Point/Actions'
18+import { BRIDGE_ERROR } from '../lib/error'
19+import { Row, Col, Warning, Button, H1, H3, Input,
20+ PointInput, InnerLabel, ValidatedSigil
21+} from '../components/Base'
22+import StatelessTransaction from '../components/StatelessTransaction'
23+
24+
25+class InvitesManage extends React.Component {
26+
27+ constructor(props) {
28+ super(props)
29+
30+ this.state = {
31+ invitesWillWork: Nothing(),
32+ planetInput: '',
33+ targetPlanet: Nothing(),
34+ currentPoolSize: Nothing(),
35+ cachedPoolSizes: {},
36+ targetPoolSize: 5
37+ }
38+
39+ this.checkCurrentInvites = this.checkCurrentInvites.bind(this);
40+ this.handlePointInput = this.handlePointInput.bind(this);
41+ this.updatePoolSize = this.updatePoolSize.bind(this);
42+ this.handlePoolSizeInput = this.handlePoolSizeInput.bind(this);
43+ this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
44+ this.statelessRef = React.createRef();
45+ }
46+
47+ componentDidMount() {
48+ this.point = this.props.pointCursor.matchWith({
49+ Just: (pt) => parseInt(pt.value, 10),
50+ Nothing: () => {
51+ throw BRIDGE_ERROR.MISSING_POINT
52+ }
53+ });
54+ this.contracts = this.props.contracts.matchWith({
55+ Just: cs => cs.value,
56+ Nothing: _ => {
57+ throw BRIDGE_ERROR.MISSING_CONTRACTS
58+ }
59+ });
60+
61+ azimuth.azimuth.isSpawnProxy(
62+ this.contracts,
63+ this.point,
64+ this.contracts.delegatedSending.address
65+ ).then(isSpawnProxy => {
66+ this.setState({invitesWillWork:Just(isSpawnProxy)});
67+ });
68+ }
69+
70+ componentDidUpdate(prevProps) {
71+ //
72+ }
73+
74+ handlePointInput(targetPlanet) {
75+ let newState = {
76+ targetPlanet: Nothing(),
77+ planetInput: targetPlanet,
78+ canGenerate: false,
79+ currentPoolSize: Nothing()
80+ }
81+ if ((targetPlanet.length === 14)
82+ && ob.isValidPatp(targetPlanet)) {
83+ const target = ob.patp2dec(targetPlanet);
84+ //TODO there must be a simpler way to do this check
85+ const isChild = ob.sein(targetPlanet) === ob.patp(this.point);
86+ if (isChild) {
87+ newState.targetPlanet = Just(target);
88+ newState.currentPoolSize = this.checkCurrentInvites(target);
89+ newState.canGenerate = true;
90+ }
91+ }
92+ this.setState(newState);
93+ }
94+
95+ checkCurrentInvites(who) {
96+ const cachedPoolSizes = this.state.cachedPoolSizes;
97+ if (typeof cachedPoolSizes[who] !== 'undefined') {
98+ return Just(cachedPoolSizes[who]);
99+ } else {
100+ azimuth.delegatedSending.getPool(this.contracts, who)
101+ .then(pool => {
102+ azimuth.delegatedSending.invitesInPool(this.contracts, pool)
103+ .then(size => {
104+ this.updatePoolSize(who, size);
105+ });
106+ });
107+ return Nothing();
108+ }
109+ }
110+
111+ updatePoolSize(who, size) {
112+ this.state.cachedPoolSizes[who] = size;
113+ const target = this.state.targetPlanet;
114+ const match = Just.hasInstance(target) ? (who === target.value) : false;
115+ if (match) {
116+ this.setState({currentPoolSize:Just(size)});
117+ }
118+ }
119+
120+ handlePoolSizeInput(targetSize) {
121+ this.setState({targetPoolSize: targetSize});
122+ }
123+
124+ createUnsignedTxn() {
125+ return Just(azimuth.delegatedSending.setPoolSize(
126+ this.contracts,
127+ this.state.targetPlanet.value,
128+ this.state.targetPoolSize
129+ ))
130+ }
131+
132+ render() {
133+
134+ let spawnProxyWarning = null;
135+ if (this.state.invitesWillWork.value === false) {
136+ spawnProxyWarning = (
137+ <Warning>
138+ <h3 className={'mb-2'}>{'Warning'}</h3>
139+ {
140+ 'Planets under this star will not be able to send invites ' +
141+ 'until the invite contract ('
142+ }<code>{this.contracts.delegatedSending.address}</code>{
143+ ') is made spawn proxy for this star.'
144+ }
145+ </Warning>
146+ );
147+ }
148+
149+ let poolSizeText = this.state.currentPoolSize.matchWith({
150+ Just: ps => `(currently ${ps.toString()})`,
151+ Nothing: _ => ''
152+ });
153+
154+ return (
155+ <Row>
156+ <Col>
157+
158+ <p>{ 'manage invites here, for stars. can only give invites to child planets' }</p>
159+
160+ { spawnProxyWarning }
161+
162+ <PointInput
163+ prop-format='innerlabel'
164+ prop-size='lg'
165+ placeholder={ '~sampel-sipnem' }
166+ value={ this.state.planetInput }
167+ onChange={ this.handlePointInput }>
168+ <InnerLabel>{ 'Planet to set invites for' }</InnerLabel>
169+ <ValidatedSigil
170+ patp={ this.state.targetPlanet }
171+ validator={() => this.validatePoint} />
172+ </PointInput>
173+
174+ <Input
175+ prop-format='innerlabel'
176+ prop-size='lg'
177+ value={ this.state.targetPoolSize }
178+ onChange={ this.handlePoolSizeInput }>
179+ <InnerLabel>
180+ { `Available invites ${poolSizeText}` }
181+ </InnerLabel>
182+ </Input>
183+
184+ <StatelessTransaction
185+ // Upper scope
186+ web3={this.props.web3}
187+ contracts={this.props.contracts}
188+ wallet={this.props.wallet}
189+ walletType={this.props.walletType}
190+ walletHdPath={this.props.walletHdPath}
191+ networkType={this.props.networkType}
192+ setTxnHashCursor={this.props.setTxnHashCursor}
193+ setTxnConfirmations={this.props.setTxnConfirmations}
194+ popRoute={this.props.popRoute}
195+ pushRoute={this.props.pushRoute}
196+ // Other
197+ canGenerate={this.state.canGenerate}
198+ createUnsignedTxn={this.createUnsignedTxn}
199+ ref={this.statelessRef} />
200+
201+ </Col>
202+ </Row>
203+ )
204+ }
205+}
206+
207+export default InvitesManage
src/bridge/views/InvitesSend.jsnew file+262
@@ -0,0 +1,262 @@
1+import { Just, Nothing } from 'folktale/maybe'
2+import React from 'react'
3+import { pour } from 'sigil-js'
4+import * as ob from 'urbit-ob'
5+import * as azimuth from 'azimuth-js'
6+
7+import {
8+ EthereumWallet,
9+ addressFromSecp256k1Public,
10+ urbitWalletFromTicket,
11+ addHexPrefix,
12+ WALLET_NAMES
13+} from '../lib/wallet'
14+
15+import ReactSVGComponents from '../components/ReactSVGComponents'
16+import KeysAndMetadata from './Point/KeysAndMetadata'
17+import Actions from './Point/Actions'
18+import { BRIDGE_ERROR } from '../lib/error'
19+import { sendMail } from '../lib/inviteMail'
20+import { Row, Col, Warning, Button, H1, H3,
21+ Input, InnerLabel
22+} from '../components/Base'
23+import StatelessTransaction from '../components/StatelessTransaction'
24+
25+// for wallet generation
26+import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
27+import * as more from 'more-entropy'
28+import lodash from 'lodash'
29+import { PLANET_ENTROPY_BITS } from '../../walletgen/lib/constants'
30+
31+// for transaction generation and signing
32+import {
33+ signTransaction,
34+ sendSignedTransaction,
35+ waitForTransactionConfirm,
36+ isTransactionConfirmed
37+} from '../lib/txn'
38+import * as tank from '../lib/tank'
39+import Tx from 'ethereumjs-tx'
40+
41+class InvitesSend extends React.Component {
42+
43+ constructor(props) {
44+ super(props)
45+
46+ this.state = {
47+ invitesAvailable: Nothing(),
48+ fromPool: Nothing(),
49+ haveInvited: Nothing(),
50+ randomPlanet: Nothing(),
51+ inviteWallet: Nothing(),
52+ targetEmail: ''
53+ }
54+
55+ this.findInvited = this.findInvited.bind(this);
56+ this.findRandomPlanet = this.findRandomPlanet.bind(this);
57+ this.generateWallet = this.generateWallet.bind(this);
58+ this.canGenerate = this.canGenerate.bind(this);
59+ this.handleEmailInput = this.handleEmailInput.bind(this);
60+ this.txnConfirmation = this.txnConfirmation.bind(this);
61+ this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
62+
63+ this.promptText = 'The email will be sent on first confirmation. Please wait.';
64+ }
65+
66+ componentDidMount() {
67+ this.point = this.props.pointCursor.matchWith({
68+ Just: (pt) => parseInt(pt.value, 10),
69+ Nothing: () => {
70+ throw BRIDGE_ERROR.MISSING_POINT
71+ }
72+ });
73+ this.contracts = this.props.contracts.matchWith({
74+ Just: cs => cs.value,
75+ Nothing: _ => {
76+ throw BRIDGE_ERROR.MISSING_CONTRACTS
77+ }
78+ });
79+
80+ azimuth.delegatedSending.getPool(this.contracts, this.point)
81+ .then(pool => {
82+ this.setState({fromPool: Just(pool)});
83+ azimuth.delegatedSending.invitesInPool(this.contracts, pool)
84+ .then(count => {
85+ this.setState({invitesAvailable: Just(count)});
86+ });
87+ });
88+ this.findInvited();
89+ this.findRandomPlanet();
90+ this.generateWallet();
91+ }
92+
93+ componentDidUpdate(prevProps) {
94+ //
95+ }
96+
97+ async findInvited() {
98+ let invited = await azimuth.delegatedSending.getInvited(
99+ this.contracts,
100+ this.point
101+ );
102+ invited = invited.map(async point => {
103+ const active = await azimuth.azimuth.isActive(this.contracts, point);
104+ console.log('point active', point, active);
105+ const res = {point: Number(point), active:active};
106+ return res;
107+ });
108+ invited = await Promise.all(invited);
109+ this.setState({haveInvited: Just(invited)});
110+ }
111+
112+ async findRandomPlanet() {
113+ let res = Nothing();
114+ const kids = await azimuth.azimuth.getUnspawnedChildren(
115+ this.contracts,
116+ azimuth.azimuth.getPrefix(this.point)
117+ );
118+ if (kids.length > 0) {
119+ const i = Math.floor(Math.random() * kids.length);
120+ res = Just(Number(kids[i]));
121+ }
122+ this.setState({randomPlanet: res});
123+ return res;
124+ }
125+
126+ //TODO pulled from walletgen/views/Generate, put into lib
127+ async generateWallet() {
128+ const makeTicket = () => {
129+
130+ const bits = PLANET_ENTROPY_BITS
131+
132+ const bytes = bits / 8
133+ const some = new Uint8Array(bytes)
134+ window.crypto.getRandomValues(some)
135+
136+ const gen = new more.Generator()
137+
138+ return new Promise((resolve, reject) => {
139+ gen.generate(bits, result => {
140+ const chunked = lodash.chunk(result, 2)
141+ const desired = chunked.slice(0, bytes) // only take required entropy
142+ const more = lodash.flatMap(desired, arr => arr[0] ^ arr[1])
143+ const entropy = lodash.zipWith(some, more, (x, y) => x ^ y)
144+ const buf = Buffer.from(entropy)
145+ const patq = ob.hex2patq(buf.toString('hex'))
146+ resolve(patq)
147+ reject('Entropy generation failed')
148+ })
149+ })
150+ };
151+
152+ const ticket = await makeTicket(0x10000); // planet-sized ticket
153+ const wallet = await kg.generateWallet({ticket, ship:0});
154+ console.log('found ticket', ticket, wallet, wallet.ownership.keys.address);
155+ this.setState({inviteWallet:
156+ Just({ticket, owner:wallet.ownership.keys.address})
157+ });
158+ }
159+
160+ handleEmailInput(email) {
161+ //TODO simple .*@.*\..* email validation
162+ this.setState({ targetEmail: email });
163+ }
164+
165+ canGenerate() {
166+ //TODO and canSend()
167+ let res = this.state.invitesAvailable.matchWith({
168+ Nothing: () => false,
169+ Just: invites => ((invites.value > 0)
170+ && Just.hasInstance(this.state.randomPlanet)
171+ && Just.hasInstance(this.state.inviteWallet))
172+ });
173+ return res;
174+ }
175+
176+ createUnsignedTxn() {
177+ const txn = azimuth.delegatedSending.sendPoint(
178+ this.contracts,
179+ this.point,
180+ this.state.randomPlanet.value,
181+ this.state.inviteWallet.value.owner
182+ );
183+ return Just(txn)
184+ }
185+
186+ txnConfirmation(txHash, confirmations) {
187+ this.props.setTxnConfirmations(txHash, confirmations);
188+ if (confirmations === 1) {
189+ sendMail(this.state.targetEmail, this.state.inviteWallet.value.ticket)
190+ .then(console.log);
191+ }
192+ }
193+
194+ render() {
195+
196+ const inviteStatus = this.state.invitesAvailable.matchWith({
197+ Nothing: () => 'Loading...',
198+ Just: (invites) => {
199+ let res = `${invites.value} invites available`;
200+ return res + this.state.fromPool.matchWith({
201+ Nothing: () => '',
202+ Just: (pool) => (pool.value == this.point)
203+ ? ''
204+ : ` (from ${ob.patp(pool.value)})`
205+ });
206+ }
207+ });
208+
209+
210+ const inviteList = this.state.haveInvited.matchWith({
211+ Nothing: () => (<li>{'Loading...'}</li>),
212+ Just: (invited) => invited.value.map(i => {
213+ return (<li>
214+ <span class="invitee">{ob.patp(i.point)}</span>:
215+ <span class="status">{i.active ? 'accepted' : 'pending'}</span>
216+ </li>);
217+ })
218+ });
219+
220+ return (
221+ <Row>
222+ <Col>
223+
224+ <p>{ 'send invites here, for planets' }</p>
225+
226+ <p>{ inviteStatus }</p>
227+
228+ <ul>{ inviteList }</ul>
229+
230+ <Input
231+ value={ this.state.targetEmail }
232+ onChange={ this.handleEmailInput }>
233+ <InnerLabel>
234+ { 'Recipient email' }
235+ </InnerLabel>
236+ </Input>
237+
238+ <StatelessTransaction
239+ // Upper scope
240+ web3={this.props.web3}
241+ contracts={this.props.contracts}
242+ wallet={this.props.wallet}
243+ walletType={this.props.walletType}
244+ walletHdPath={this.props.walletHdPath}
245+ networkType={this.props.networkType}
246+ setTxnHashCursor={this.props.setTxnHashCursor}
247+ setTxnConfirmations={this.txnConfirmation}
248+ popRoute={this.props.popRoute}
249+ pushRoute={this.props.pushRoute}
250+ // Other
251+ promptText={this.promptText}
252+ canGenerate={this.canGenerate()}
253+ createUnsignedTxn={this.createUnsignedTxn}
254+ ref={this.statelessRef} />
255+
256+ </Col>
257+ </Row>
258+ )
259+ }
260+}
261+
262+export default InvitesSend
src/bridge/views/IssueChild.js+2−1
@@ -253,7 +253,7 @@ class IssueChild extends React.Component {
253253 <ValidatedSigil
254254 className={'tr-0 mt-05 mr-0 abs'}
255255 patp={state.desiredPoint}
256− size={76}
256+ size={68}
257257 margin={8}
258258 validator={() => this.validatePoint(state.desiredPoint)} />
259259 </PointInput>
@@ -303,6 +303,7 @@ class IssueChild extends React.Component {
303303 walletHdPath={props.walletHdPath}
304304 networkType={props.networkType}
305305 setTxnHashCursor={props.setTxnHashCursor}
306+ setTxnConfirmations={props.setTxnConfirmations}
306307 popRoute={props.popRoute}
307308 pushRoute={props.pushRoute}
308309 // Other
src/bridge/views/Network.js+47−99
@@ -1,11 +1,7 @@
11import Maybe from 'folktale/maybe'
22import React from 'react'
33import { Button } from '../components/Base'
4−import {
5− DropdownItem,
6− InnerLabelDropdown,
7− DropdownDivider,
8−} from '../components/Base'
4+import { InnerLabelDropdown } from '../components/Base'
95import { Row, Col, H1, P } from '../components/Base'
106import * as azimuth from 'azimuth-js'
117import Web3 from 'web3'
@@ -19,21 +15,9 @@ class Network extends React.Component {
1915 constructor(props) {
2016 super(props)
2117
22− this.state = {
23− dropdownOpen: false
24− };
25−
26− this.toggle = this.toggle.bind(this);
27− this.close = this.close.bind(this);
2818 this.handleNetworkChange = this.handleNetworkChange.bind(this);
2919 }
3020
31− toggle() {
32− this.setState((state, _) => ({
33− dropdownOpen: !state.dropdownOpen
34− }))
35− }
36−
3721 handleNetworkChange(network) {
3822 const { setNetworkType, setNetwork } = this.props
3923
@@ -86,7 +70,10 @@ class Network extends React.Component {
8670 // We may want to offer the ability to select a target network for
8771 // transactions when offline.
8872
89− const web3 = new Web3()
73+ // Note: localhost:3456 doesn't actually point to anything, we just need
74+ // a provider to initialize the Web3 object
75+ const provider = new Web3.providers.HttpProvider("http://localhost:3456")
76+ const web3 = new Web3(provider)
9077
9178 const target =
9279 process.env.NODE_ENV === 'development'
@@ -99,97 +86,58 @@ class Network extends React.Component {
9986 }
10087 }
10188
102− selectClose(f) {
103− f()
104− this.setState({ dropdownOpen: false })
105− }
106−
107−
108− close () {
109− this.setState({ dropdownOpen: false })
89+ getNetworkOptions() {
90+ return [{
91+ title: 'Main Network (default)',
92+ value: NETWORK_NAMES.MAINNET
93+ }, {
94+ title: 'Local Node',
95+ value: NETWORK_NAMES.LOCAL
96+ }, {
97+ title: 'Ropsten',
98+ value: NETWORK_NAMES.ROPSTEN
99+ }, {
100+ type: 'divider'
101+ }, {
102+ title: 'Offline',
103+ value: NETWORK_NAMES.OFFLINE
104+ }]
110105 }
111106
112107 render() {
113− const { dropdownOpen } = this.state
114108 const { networkType, pushRoute } = this.props
109+ const networkOptions = this.getNetworkOptions()
115110
116111 return (
117112 <Row>
118113 <Col>
119114 <H1>{ 'Select Network' }</H1>
120115
121−
122− <P>
123− {
124− "Please select the Ethereum node you'd like to send " +
125− "transactions to. For highly valuable keys, please select " +
126− "offline mode."
127− }
128− </P>
129−
130−
131− <InnerLabelDropdown
132− className={'mt-6'}
133− handleToggle={this.toggle}
134− handleClose={this.close}
135− isOpen={dropdownOpen}
136− title={'Node:'}
137− currentSelectionTitle={renderNetworkType(networkType)}
138− >
139−
140− <DropdownItem
141− isSelected={ false }
142− onClick={
143− () => this.selectClose(() => this.handleNetworkChange(NETWORK_NAMES.MAINNET))
144− }
145− >
146− { 'Main Network (default)' }
147− </DropdownItem>
148−
149− <DropdownItem
150− isSelected={false}
151− onClick={
152− () => this.selectClose(() => this.handleNetworkChange(NETWORK_NAMES.LOCAL))
153− }
154− >
155− { //'Ganache Node @ Port 8545 (Default)'
156− }
157− {'Local Node'}
158− </DropdownItem>
159−
160− <DropdownItem
161− // disabled={ true }
162− isSelected={ false }
163− onClick={
164− () => this.selectClose(() => this.handleNetworkChange(NETWORK_NAMES.ROPSTEN))
165− }
166− >
167− { 'Ropsten' }
168− </DropdownItem>
169−
170− <DropdownDivider />
171−
172− <DropdownItem
173− // disabled={ true }
174− isSelected={ false }
175− onClick={
176− () => this.selectClose(() => this.handleNetworkChange(NETWORK_NAMES.OFFLINE))
177− }
178− >
179− { 'Offline' }
180− </DropdownItem>
181−
182− </InnerLabelDropdown>
183−
184− <Button
185− className={'mt-10'}
186− onClick={ () => pushRoute(ROUTE_NAMES.WALLET) }
187− >
188− { 'Continue →' }
189− </Button>
190−
191− </Col>
192− </Row>
116+ <P>
117+ {
118+ "Please select the Ethereum node you'd like to send " +
119+ "transactions to. For highly valuable keys, please select " +
120+ "offline mode."
121+ }
122+ </P>
123+
124+ <InnerLabelDropdown
125+ className={'mt-6'}
126+ title={'Node:'}
127+ handleUpdate={this.handleNetworkChange}
128+ options={networkOptions}
129+ currentSelectionTitle={renderNetworkType(networkType)}>
130+ </InnerLabelDropdown>
131+
132+ <Button
133+ className={'mt-10'}
134+ onClick={ () => pushRoute(ROUTE_NAMES.WALLET) }
135+ >
136+ { 'Continue →' }
137+ </Button>
138+
139+ </Col>
140+ </Row>
193141 )
194142 }
195143}
src/bridge/views/Point.js+5−5
@@ -1,4 +1,4 @@
1−import Maybe from 'folktale/maybe'
1+import { Just, Nothing } from 'folktale/maybe'
22import React from 'react'
33import { pour } from 'sigil-js'
44import * as ob from 'urbit-ob'
@@ -85,8 +85,8 @@ class Point extends React.Component {
8585
8686 const pointDetails =
8787 point in pointCache
88− ? Maybe.Just(pointCache[point])
89− : Maybe.Nothing()
88+ ? Just(pointCache[point])
89+ : Nothing()
9090
9191 const name = ob.patp(point)
9292
@@ -96,9 +96,9 @@ class Point extends React.Component {
9696 size: 256
9797 })
9898
99− const online = Maybe.Just.hasInstance(web3)
99+ const online = Just.hasInstance(web3)
100100
101− const authenticated = Maybe.Just.hasInstance(wallet)
101+ const authenticated = Just.hasInstance(wallet)
102102
103103 const routeHandler = route => {
104104 popRoute()
src/bridge/views/Point/Actions.js+56−23
@@ -9,10 +9,12 @@ import { BRIDGE_ERROR } from '../../lib/error'
99import { Row, Col, H2, P } from '../../components/Base'
1010import { Button } from '../../components/Base'
1111import { ROUTE_NAMES } from '../../lib/router'
12−
12+import { azimuth } from 'azimuth-js'
1313
1414const isPlanet = point =>
15− parseInt(point, 10) > 65535
15+ (azimuth.getPointSize(point) === azimuth.PointSize.Planet);
16+const isStar = point =>
17+ (azimuth.getPointSize(point) === azimuth.PointSize.Star);
1618
1719const Actions = (props) => {
1820 const { pushRoute, online, point, pointDetails, wallet } = props
@@ -24,35 +26,30 @@ const Actions = (props) => {
2426 }
2527 })
2628
27− const canSetSpawnProxy = pointDetails.matchWith({
29+ const isOwner = pointDetails.matchWith({
2830 Nothing: _ => false,
29− Just: details =>
30− eqAddr(details.value.owner, addr) && details.value.active
31− })
32−
33− const canSetManagementProxy = pointDetails.matchWith({
31+ Just: details => eqAddr(details.value.owner, addr)
32+ });
33+ const isActive = pointDetails.matchWith({
3434 Nothing: _ => false,
35− Just: details =>
36− eqAddr(details.value.owner, addr) && details.value.active
37− })
35+ Just: details => details.value.active
36+ });
37+ const isActiveOwner = (isOwner && isActive);
38+ const canSetSpawnProxy = isActiveOwner;
39+ const canSetManagementProxy = isActiveOwner;
3840
39− const canConfigureKeys = pointDetails.matchWith({
41+ const canManage = (isOwner || pointDetails.matchWith({
4042 Nothing: _ => false,
41− Just: details => {
42− const canManage =
43− eqAddr(details.value.owner, addr)
44− || eqAddr(details.value.managementProxy, addr)
45−
46− return canManage && details.value.active
47− }
48− })
43+ Just: details => eqAddr(details.value.managementProxy, addr)
44+ }));
45+ const canConfigureKeys = (canManage && isActive);
4946
5047 const canIssueChild =
5148 pointDetails.matchWith({
5249 Nothing: () => false,
5350 Just: (details) => {
5451 const hasPermission =
55− eqAddr(details.value.owner, addr)
52+ isOwner
5653 || eqAddr(details.value.spawnProxy, addr)
5754
5855 const isBooted =
@@ -65,6 +62,7 @@ const Actions = (props) => {
6562 })
6663
6764 const planet = isPlanet(point)
65+ const star = isStar(point)
6866
6967 const canTransfer =
7068 pointDetails.matchWith({
@@ -112,6 +110,36 @@ const Actions = (props) => {
112110 }
113111 })
114112
113+ let invites = null;
114+ if (planet) {
115+ invites = (
116+ <Button
117+ disabled={(!isActiveOwner || !online)}
118+ prop-size={'sm'}
119+ prop-type={'link'}
120+ onClick={ () => {
121+ pushRoute(ROUTE_NAMES.INVITES_SEND);
122+ }}
123+ >
124+ { 'Send invites' }
125+ </Button>
126+ );
127+ }
128+ if (star) {
129+ invites = (
130+ <Button
131+ disabled={(!(isActiveOwner && canIssueChild) || !online)}
132+ prop-size={'sm'}
133+ prop-type={'link'}
134+ onClick={ () => {
135+ pushRoute(ROUTE_NAMES.INVITES_MANAGE);
136+ }}
137+ >
138+ { 'Manage invites' }
139+ </Button>
140+ );
141+ }
142+
115143 return (
116144 <div>
117145 <H2>{ 'Actions' }</H2>
@@ -137,7 +165,7 @@ const Actions = (props) => {
137165 <Button
138166 prop-size={'sm'}
139167 prop-type={'link'}
140− disabled={ !canAcceptTransfer }
168+ disabled={ online && !canAcceptTransfer }
141169 onClick={ () => {
142170 pushRoute(ROUTE_NAMES.ACCEPT_TRANSFER)
143171 }}
@@ -159,7 +187,7 @@ const Actions = (props) => {
159187 <Button
160188 prop-size={'sm'}
161189 prop-type={'link'}
162− disabled={ !canGenKeyfile }
190+ disabled={ online && !canGenKeyfile }
163191 onClick={ () => {
164192 pushRoute(ROUTE_NAMES.GEN_KEYFILE)
165193 }}
@@ -215,6 +243,11 @@ const Actions = (props) => {
215243 </Button>
216244
217245 </Col>
246+ <Col className={'flex flex-column items-start col-md-4'}>
247+
248+ { invites }
249+
250+ </Col>
218251 </Row>
219252 </div>
220253 )
src/bridge/views/Points.js+5−1
@@ -4,6 +4,7 @@ import { Button } from '../components/Base'
44import * as azimuth from 'azimuth-js'
55
66import PointList from '../components/PointList'
7+import { NETWORK_NAMES } from '../lib/network'
78import { ROUTE_NAMES } from '../lib/router'
89import {
910 ETH_ZERO_ADDR,
@@ -21,6 +22,9 @@ class Points extends React.Component {
2122 constructor(props) {
2223 super(props)
2324
25+ const { networkType } = props
26+ const loading = networkType !== NETWORK_NAMES.OFFLINE
27+
2428 this.state = {
2529 points: [],
2630 incoming: [],
@@ -28,7 +32,7 @@ class Points extends React.Component {
2832 voting: [],
2933 spawning: [],
3034 eclipticOwner: false,
31− loading: true
35+ loading: loading
3236 }
3337 }
3438
src/bridge/views/SentTransaction.js+104−44
@@ -1,3 +1,4 @@
1+import Maybe from 'folktale/maybe'
12import React from 'react'
23import { Row, Col, H1, H3, P, Warning, Anchor } from '../components/Base'
34import { Button } from '../components/Base'
@@ -6,55 +7,100 @@ import { ROUTE_NAMES } from '../lib/router'
67import { BRIDGE_ERROR, renderTxnError } from '../lib/error'
78import { NETWORK_NAMES } from '../lib/network'
89
9−const Success = (props) => {
10−
11− const esvisible =
12− props.networkType === NETWORK_NAMES.ROPSTEN ||
13− props.networkType === NETWORK_NAMES.MAINNET
14−
15− const esdomain =
16− props.networkType === NETWORK_NAMES.ROPSTEN
17− ? "ropsten.etherscan.io"
18− : "etherscan.io"
19−
20− const esmessage =
21− esvisible === true
22− ? "If you’d like to keep track of it, click the Etherscan link below."
23− : ''
24−
25− const esanchor =
26− esvisible === false
27− ? <div />
28− : <Anchor
29− className={'mb-4 mt-1'}
30− prop-size={'sm'}
31− target={'_blank'}
32− href={`https://${esdomain}/tx/${props.hash}`}>
33− {'View on Etherscan ↗'}
34− </Anchor>
3510
36− return (
37− <Row>
38− <Col>
39− <H1>{ 'Your Transaction was Sent' }</H1>
11+class Success extends React.Component {
12+
13+ constructor(props) {
14+ super(props)
15+
16+ this.state = {
17+ pending: '.',
18+ interval: null
19+ }
20+ }
21+
22+ componentDidMount() {
23+ const nextDot = {'.': '..',
24+ '..': '...',
25+ '...': '.'}
26+
27+ const interval = setInterval(() => {
28+ this.setState(({ pending }) => ({pending: nextDot[pending]}))
29+ }, 1000)
30+ this.setState({interval: interval})
31+ }
32+
33+ componentWillUnmount() {
34+ clearInterval(this.state.interval)
35+ }
36+
37+ render() {
38+
39+ const { props, state } = this
40+ const { networkType, hash, txnConfirmations } = props
41+ const { pending } = state
42+
43+ const esvisible =
44+ networkType === NETWORK_NAMES.ROPSTEN ||
45+ networkType === NETWORK_NAMES.MAINNET
46+
47+ const esdomain =
48+ networkType === NETWORK_NAMES.ROPSTEN
49+ ? "ropsten.etherscan.io"
50+ : "etherscan.io"
51+
52+ const esmessage =
53+ esvisible === true
54+ ? "If you’d like to keep track of it, click the Etherscan link below."
55+ : ''
56+
57+ const esanchor =
58+ esvisible === false
59+ ? null
60+ : <Anchor
61+ className={'mb-4 mt-1'}
62+ prop-size={'sm'}
63+ target={'_blank'}
64+ href={`https://${esdomain}/tx/${hash}`}>
65+ {'View on Etherscan ↗'}
66+ </Anchor>
67+
68+ const confirmations = Maybe.fromNullable(txnConfirmations[hash]).getOrElse(0)
69+
70+ const requiredConfirmations = 1
71+
72+ const status = confirmations < requiredConfirmations ?
73+ `Pending${pending}` : `Confirmed! (x${confirmations} confirmations)!`
4074
41− <P>
42− {
43− `We sent your transaction to the chain. It can take some time to
75+ return (
76+ <Row>
77+ <Col>
78+ <H1>{ 'Your Transaction was Sent' }</H1>
79+
80+ <P>
81+ {
82+ `We sent your transaction to the chain. It can take some time to
4483 execute, especially if the network is busy. ${esmessage}`
45− }
46− </P>
84+ }
85+ </P>
4786
48− <H3>{ 'Transaction Hash' }</H3>
49− <P>
50− { props.hash }
51− </P>
87+ <H3>{ 'Transaction Hash' }</H3>
88+ <P>
89+ { hash }
90+ </P>
5291
53− { esanchor }
92+ <H3>{ 'Transaction Status' }</H3>
93+ <P>
94+ { status }
95+ </P>
96+ <P>
97+ { esanchor }
98+ </P>
5499
55− </Col>
56− </Row>
57− )
100+ </Col>
101+ </Row>
102+ )
103+ }
58104}
59105
60106const Failure = (props) =>
@@ -76,10 +122,11 @@ const Failure = (props) =>
76122 </Row>
77123
78124const SentTransaction = (props) => {
79− const { web3, txnHashCursor, networkType, popRoute, pushRoute } = props
125+ const { web3, txnHashCursor, networkType, popRoute, pushRoute, txnConfirmations} = props
80126 const { setPointCursor, pointCursor } = props
81127
82128 const promptKeyfile = props.routeData && props.routeData.promptKeyfile
129+ const promptText = props.routeData && props.routeData.promptText
83130
84131 const w3 = web3.matchWith({
85132 Nothing: _ => { throw BRIDGE_ERROR.MISSING_WEB3 },
@@ -97,9 +144,21 @@ const SentTransaction = (props) => {
97144 <Success
98145 hash={ hash.value }
99146 networkType={ networkType }
147+ txnConfirmations={ txnConfirmations }
100148 />
101149 })
102150
151+ let text;
152+ if (promptText) {
153+ text = (
154+ <Row>
155+ <Col>
156+ <p>{promptText}</p>
157+ </Col>
158+ </Row>
159+ )
160+ }
161+
103162 const ok =
104163 <Row>
105164 <Col>
@@ -140,6 +199,7 @@ const SentTransaction = (props) => {
140199 return (
141200 <div>
142201 { body }
202+ { text }
143203 { ok }
144204 { keyfile }
145205 </div>
src/bridge/views/SetKeys.js+1
@@ -194,6 +194,7 @@ class SetKeys extends React.Component {
194194 walletHdPath={props.walletHdPath}
195195 networkType={props.networkType}
196196 setTxnHashCursor={props.setTxnHashCursor}
197+ setTxnConfirmations={props.setTxnConfirmations}
197198 popRoute={props.popRoute}
198199 pushRoute={props.pushRoute}
199200 // Tx
src/bridge/views/SetProxy.js+1
@@ -162,6 +162,7 @@ class SetProxy extends React.Component {
162162 walletHdPath={props.walletHdPath}
163163 networkType={props.networkType}
164164 setTxnHashCursor={props.setTxnHashCursor}
165+ setTxnConfirmations={props.setTxnConfirmations}
165166 popRoute={props.popRoute}
166167 pushRoute={props.pushRoute}
167168 // Other
src/bridge/views/Transfer.js+1
@@ -132,6 +132,7 @@ class Transfer extends React.Component {
132132 walletHdPath={props.walletHdPath}
133133 networkType={props.networkType}
134134 setTxnHashCursor={props.setTxnHashCursor}
135+ setTxnConfirmations={props.setTxnConfirmations}
135136 popRoute={props.popRoute}
136137 pushRoute={props.pushRoute}
137138 // Checks
src/bridge/views/Wallet.js+32−84
@@ -1,52 +1,50 @@
11import Maybe from 'folktale/maybe'
22import React from 'react'
33import { Button, H1, P } from '../components/Base'
4−import {
5− InnerLabelDropdown,
6− DropdownItem,
7− DropdownDivider
8−} from '../components/Base'
4+import { InnerLabelDropdown } from '../components/Base'
95import { Row, Col } from '../components/Base'
106
117import { ROUTE_NAMES } from '../lib/router'
128import { WALLET_NAMES, renderWalletType } from '../lib/wallet'
139
1410class Wallet extends React.Component {
15−
16− constructor(props) {
17− super(props)
18−
19− this.state = {
20− dropdownOpen: false
21− }
22−
23− this.toggle = this.toggle.bind(this)
24− this.close = this.close.bind(this)
25−
26− }
27−
2811 componentDidMount() {
2912 const { setWallet } = this.props
3013 setWallet(Maybe.Nothing())
3114 }
3215
33− toggle() {
34− this.setState((state, _) => ({
35− dropdownOpen: !state.dropdownOpen
36− }))
37− }
38−
39− selectClose(f) {
40− f()
41− this.setState({ dropdownOpen: false })
42− }
43−
44− close () {
45− this.setState({ dropdownOpen: false })
16+ getWalletOptions() {
17+ return [{
18+ title: 'Urbit Master Ticket',
19+ value: WALLET_NAMES.TICKET
20+ }, {
21+ title: 'Urbit Master Ticket (Shards)',
22+ value: WALLET_NAMES.SHARDS
23+ }, {
24+ type: 'divider'
25+ }, {
26+ title: 'Ledger',
27+ value: WALLET_NAMES.LEDGER
28+ }, {
29+ title: 'Trezor',
30+ value: WALLET_NAMES.TREZOR
31+ }, {
32+ type: 'divider'
33+ }, {
34+ title: 'BIP39 Mnemonic',
35+ value: WALLET_NAMES.MNEMONIC
36+ }, {
37+ title: 'Ethereum Private Key',
38+ value: WALLET_NAMES.PRIVATE_KEY
39+ }, {
40+ title: 'Ethereum Keystore File',
41+ value: WALLET_NAMES.KEYSTORE
42+ }]
4643 }
4744
4845 render() {
49− const { props, state } = this
46+ const { props } = this
47+ const walletOptions = this.getWalletOptions()
5048
5149 return (
5250 <Row>
@@ -63,60 +61,10 @@ class Wallet extends React.Component {
6361
6462 <InnerLabelDropdown
6563 className={'mb-10 mt-6'}
66− handleToggle={this.toggle}
67− handleClose={this.close}
68− isOpen={state.dropdownOpen}
6964 title={'Wallet Type:'}
65+ options={walletOptions}
66+ handleUpdate={props.setWalletType}
7067 currentSelectionTitle={renderWalletType(props.walletType)}>
71−
72− <DropdownItem
73− onClick={ () => this.selectClose(() => props.setWalletType(WALLET_NAMES.TICKET)) }
74− >
75− { 'Urbit Master Ticket' }
76− </DropdownItem>
77−
78− <DropdownItem
79− onClick={ () => this.selectClose(() => props.setWalletType(WALLET_NAMES.SHARDS)) }
80− >
81− { 'Urbit Master Ticket (Shards)' }
82− </DropdownItem>
83−
84− <DropdownDivider />
85−
86− <DropdownItem
87− onClick={ () => this.selectClose(() => props.setWalletType(WALLET_NAMES.LEDGER)) }
88− >
89− { 'Ledger' }
90− </DropdownItem>
91−
92− <DropdownItem
93− onClick={ () => this.selectClose(() => props.setWalletType(WALLET_NAMES.TREZOR)) }
94− >
95− { 'Trezor' }
96− </DropdownItem>
97−
98− <DropdownDivider />
99−
100− <DropdownItem
101− onClick={ () => this.selectClose(() => props.setWalletType(WALLET_NAMES.MNEMONIC)) }
102− >
103− { 'BIP39 Mnemonic' }
104− </DropdownItem>
105−
106− <DropdownItem
107− onClick={ () => this.selectClose(() => props.setWalletType(WALLET_NAMES.PRIVATE_KEY)) }
108− >
109− { 'Ethereum Private Key' }
110− </DropdownItem>
111−
112− <DropdownItem
113− onClick={ () => this.selectClose(() => props.setWalletType(WALLET_NAMES.KEYSTORE)) }
114− >
115− { 'Ethereum Keystore File' }
116− </DropdownItem>
117−
118−
119−
12068 </InnerLabelDropdown>
12169
12270 <Button