on master help sign in
Merged
32a3c99Chris Johnson
CHANGELOG+17−1
@@ -1,6 +1,22 @@
1
1
2# Changelog
2# Changelog
3
3
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+
4- 1.2.1 (2019-03-21)
20- 1.2.1 (2019-03-21)
5 This release fixes an issue with the spawning of child points.
21 This release fixes an issue with the spawning of child points.
6
22
@@ -29,4 +45,4 @@
29
45
30 * Add option for setting Trezor and Ledger custom derivation path
46 * Add option for setting Trezor and Ledger custom derivation path
31 (instead of defaulting to 0 index address)
47 (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.
4
4
5## Usage
5## Usage
6
6
7−To run Bridge, follow the instructions on the [latest release][rele] page.
7+### Requirements
8
8
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+
9−## Development Notes
49+## Development notes
10
50
11### Install / Build
51### Install / Build
12
52
@@ -32,22 +72,7 @@ $ nvm use v11.0.0
32
72
33before running `npm run pilot`.
73before running `npm run pilot`.
34
74
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
51
76
52The ecliptic owner is the only account that's able to create galaxies, so
77The ecliptic owner is the only account that's able to create galaxies, so
53it's a good place to get started. On the testnet, it's the address:
78it's a good place to get started. On the testnet, it's the address:
@@ -81,7 +106,7 @@ with 100 ETH:
81To play around with any of these, authenticate using the same mnemonic, but
106To play around with any of these, authenticate using the same mnemonic, but
82use a custom HD path of `m/44'/60'/0'/0/1`, `m/44'/60'/0'/0/2`, and so on.
107use a custom HD path of `m/44'/60'/0'/0/1`, `m/44'/60'/0'/0/2`, and so on.
83
108
84−### Initial Development State
109+### Initial development state
85
110
86You can also tweak a couple of things to change your development state
111You can also tweak a couple of things to change your development state
87somewhat (say, for example, you want to start on the points list screen, instead
112somewhat (say, for example, you want to start on the points list screen, instead
bridge-https.py−1
@@ -4,5 +4,4 @@ import ssl
4
4
5httpd = BaseHTTPServer.HTTPServer(('localhost', 4443), SimpleHTTPServer.SimpleHTTPRequestHandler)
5httpd = BaseHTTPServer.HTTPServer(('localhost', 4443), SimpleHTTPServer.SimpleHTTPRequestHandler)
6httpd.socket = ssl.wrap_socket (httpd.socket, certfile='localhost.pem', keyfile='localhost-key.pem', server_side=True)
6httpd.socket = ssl.wrap_socket (httpd.socket, certfile='localhost.pem', keyfile='localhost-key.pem', server_side=True)
7−os.chdir('./build')
8httpd.serve_forever()
7httpd.serve_forever()
package.json+9−9
@@ -1,6 +1,6 @@
1{
1{
2 "name": "bridge",
2 "name": "bridge",
3− "version": "1.2.1",
3+ "version": "1.3.1",
4 "description": "An application for interacting with Azimuth",
4 "description": "An application for interacting with Azimuth",
5 "repository": {
5 "repository": {
6 "type": "git",
6 "type": "git",
@@ -14,10 +14,11 @@
14 "node": "11.0.0"
14 "node": "11.0.0"
15 },
15 },
16 "dependencies": {
16 "dependencies": {
17− "azimuth-js": "^0.10.0",
18 "@ledgerhq/hw-app-eth": "^4.35.0",
17 "@ledgerhq/hw-app-eth": "^4.35.0",
19 "@ledgerhq/hw-transport-u2f": "^4.35.0",
18 "@ledgerhq/hw-transport-u2f": "^4.35.0",
19+ "PaperCollateralRenderer": "github:urbit/PaperCollateralRenderer",
20− "azimuth-solidity": "1.0.2",
20+ "azimuth-js": "^0.13.2",
21+ "azimuth-solidity": "^1.1.0",
21 "babel-polyfill": "^6.26.0",
22 "babel-polyfill": "^6.26.0",
22 "bip32": "^1.0.2",
23 "bip32": "^1.0.2",
23 "bip39": "^2.5.0",
24 "bip39": "^2.5.0",
@@ -34,7 +35,6 @@
34 "keythereum": "^1.0.4",
35 "keythereum": "^1.0.4",
35 "lodash": "^4.17.11",
36 "lodash": "^4.17.11",
36 "more-entropy": "^0.0.7",
37 "more-entropy": "^0.0.7",
37− "PaperCollateralRenderer": "github:urbit/PaperCollateralRenderer",
38 "prop-types": "^15.6.2",
38 "prop-types": "^15.6.2",
39 "react": "^16.6.0",
39 "react": "^16.6.0",
40 "react-dom": "^16.6.0",
40 "react-dom": "^16.6.0",
@@ -45,18 +45,18 @@
45 "trezor-connect": "^7.0.1",
45 "trezor-connect": "^7.0.1",
46 "urbit-key-generation": "0.12.1",
46 "urbit-key-generation": "0.12.1",
47 "urbit-ob": "^3.1.0",
47 "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"
49 },
50 },
50 "scripts": {
51 "scripts": {
51 "start": "react-scripts start",
52 "start": "react-scripts start",
52 "build": "react-scripts build",
53 "build": "react-scripts build",
53 "test": "react-scripts test --env=jsdom",
54 "test": "react-scripts test --env=jsdom",
54 "eject": "react-scripts eject",
55 "eject": "react-scripts eject",
55− "checksum": "find build -exec shasum -a256 {} ';' > checksums.txt",
56+ "checksum": "cd build && find . -exec shasum -a256 {} ';' > ../checksums.txt && cd ..",
56− "tar": "tar -czvf bridge-$npm_package_version.tar.gz README.md bridge-https.py checksums.txt build",
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 ..",
57− "zip": "zip -FSr bridge-$npm_package_version.zip README.md bridge-https.py checksums.txt build",
58 "release": "npm install && npm run build && npm run checksum && npm run zip",
58 "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 &",
60 "pilot:deploy": "cd node_modules/azimuth-solidity && truffle deploy",
60 "pilot:deploy": "cd node_modules/azimuth-solidity && truffle deploy",
61 "pilot:setup": "npm run pilot:ganache && npm run pilot:deploy",
61 "pilot:setup": "npm run pilot:ganache && npm run pilot:deploy",
62 "pilot": "HTTPS=true npm-run-all pilot:setup start pilot:cleanup --continue-on-error",
62 "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'
9
9
10import './common/style/index.css'
10import './common/style/index.css'
11import './bridge/style/index.css'
11import './bridge/style/index.css'
12−import './walletgen/style/index.css'
13
12
14class Root extends React.Component {
13class Root extends React.Component {
15 constructor(props) {
14 constructor(props) {
@@ -88,10 +87,6 @@ class Root extends React.Component {
88 return (
87 return (
89 <React.Fragment>
88 <React.Fragment>
90 {appComponent}
89 {appComponent}
91− <AppNavigation
92− setView={this.setView}
93− appView={this.state.appView}
94− />
95 </React.Fragment>
90 </React.Fragment>
96 )
91 )
97 }
92 }
src/bridge/Bridge.js+18−3
@@ -25,7 +25,11 @@ const initWeb3 = (networkType) => {
25 const contracts = azimuth.initContracts(web3, CONTRACT_ADDRESSES.MAINNET)
25 const contracts = azimuth.initContracts(web3, CONTRACT_ADDRESSES.MAINNET)
26 return {web3: web3, contracts: contracts}
26 return {web3: web3, contracts: contracts}
27 } else if (networkType === NETWORK_NAMES.LOCAL) {
27 } else if (networkType === NETWORK_NAMES.LOCAL) {
28+ const protocol =
29+ process.env.NODE_ENV === 'development'
30+ ? 'ws'
31+ : 'wss'
28− const endpoint = 'ws://localhost:8545'
32+ const endpoint = `${protocol}://localhost:8545`
29 const provider = new Web3.providers.WebsocketProvider(endpoint)
33 const provider = new Web3.providers.WebsocketProvider(endpoint)
30 const web3 = new Web3(provider)
34 const web3 = new Web3(provider)
31 const contracts = azimuth.initContracts(web3, CONTRACT_ADDRESSES.DEV)
35 const contracts = azimuth.initContracts(web3, CONTRACT_ADDRESSES.DEV)
@@ -80,7 +84,8 @@ class Bridge extends React.Component {
80 pointCursor: Nothing(),
84 pointCursor: Nothing(),
81 pointCache: {},
85 pointCache: {},
82 // txn
86 // txn
83− txnHashCursor: Nothing()
87+ txnHashCursor: Nothing(),
88+ txnConfirmations: {}
84 }
89 }
85
90
86 this.pushRoute = this.pushRoute.bind(this)
91 this.pushRoute = this.pushRoute.bind(this)
@@ -95,6 +100,7 @@ class Bridge extends React.Component {
95 this.setAuthMnemonic = this.setAuthMnemonic.bind(this)
100 this.setAuthMnemonic = this.setAuthMnemonic.bind(this)
96 this.setPointCursor = this.setPointCursor.bind(this)
101 this.setPointCursor = this.setPointCursor.bind(this)
97 this.setTxnHashCursor = this.setTxnHashCursor.bind(this)
102 this.setTxnHashCursor = this.setTxnHashCursor.bind(this)
103+ this.setTxnConfirmations = this.setTxnConfirmations.bind(this)
98 this.setNetworkSeedCache = this.setNetworkSeedCache.bind(this)
104 this.setNetworkSeedCache = this.setNetworkSeedCache.bind(this)
99 this.addToPointCache = this.addToPointCache.bind(this)
105 this.addToPointCache = this.addToPointCache.bind(this)
100 }
106 }
@@ -244,6 +250,12 @@ class Bridge extends React.Component {
244 this.setState({ txnHashCursor })
250 this.setState({ txnHashCursor })
245 }
251 }
246
252
253+ setTxnConfirmations(txnHash, txnConfirmations) {
254+ this.setState((prevState, _) =>
255+ ({txnConfirmations: { ...prevState.txnConfirmations,
256+ [txnHash]: txnConfirmations}}))
257+ }
258+
247 render() {
259 render() {
248 const {
260 const {
249 routeCrumbs,
261 routeCrumbs,
@@ -258,6 +270,7 @@ class Bridge extends React.Component {
258 pointCursor,
270 pointCursor,
259 pointCache,
271 pointCache,
260 txnHashCursor,
272 txnHashCursor,
273+ txnConfirmations,
261 web3,
274 web3,
262 contracts
275 contracts
263 } = this.state
276 } = this.state
@@ -319,7 +332,9 @@ class Bridge extends React.Component {
319 setNetworkSeedCache= { this.setNetworkSeedCache }
332 setNetworkSeedCache= { this.setNetworkSeedCache }
320 // txn
333 // txn
321 setTxnHashCursor={ this.setTxnHashCursor }
334 setTxnHashCursor={ this.setTxnHashCursor }
322− txnHashCursor={ txnHashCursor } />
335+ txnHashCursor={ txnHashCursor }
336+ setTxnConfirmations={ this.setTxnConfirmations }
337+ txnConfirmations={ txnConfirmations } />
323
338
324 <div className={'push'} />
339 <div className={'push'} />
325
340
src/bridge/components/InnerLabelDropdown.js+88−45
@@ -1,71 +1,114 @@
1import React from 'react'
1import React from 'react'
2import PropTypes from 'prop-types';
2import PropTypes from 'prop-types';
3
3
4+import { DropdownItem, DropdownDivider } from '../components/Base'
4
5
5−const InnerLabelDropdown = props => {
6+class InnerLabelDropdown extends React.Component {
6−
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+
7− const menuVisibility = props.isOpen === true
54+ const menuVisibility = state.isOpen === true
8− ? 'visible'
55+ ? 'visible'
9− : 'hidden';
56+ : 'hidden';
10−
57+
11− const selectedClasses = props.isOpen === true
58+ const selectedClasses = state.isOpen === true
12− ? 'br-blue'
59+ ? 'br-blue'
13− : 'br-gray-50';
60+ : 'br-gray-50';
14−
61+
62+ const fullWidthClass = props.fullWidth ? 'full-width' : ''
63+
15− return (
64+ return (
16− <div className={'rel table'}>
65+ <div className={`rel table ${fullWidthClass}`}>
17− <div
66+ <div
18− className={`fix full o-0 ${menuVisibility}`}
67+ className={`fix full o-0 ${menuVisibility}`}
19− onClick={props.handleClose}
68+ onClick={this.close}
20− />
69+ />
21−
70+
22− <button
71+ <button
23− className={`h-10 mp-0 bg-white outline-blue sans ${props.className}`}
72+ className={`h-10 mp-0 bg-white outline-blue sans ${props.className} ${fullWidthClass}`}
24− onClick={props.handleToggle}
73+ onClick={this.toggle}
25− disabled={props.disabled}>
74+ disabled={props.disabled}>
26− <div className={'flex'}>
75+ <div className={'flex'}>
27− <div className={`ph-4 flex-center-all b-2 br-0 b-solid h-10 ${selectedClasses}`}>
76+ <div className={`ph-4 flex align-center b-2 br-0 b-solid h-10 ${selectedClasses} ${fullWidthClass}`}>
28− <div className={'text-400 mr-2'}>{props.title}</div>
77+ <div className={'text-400 mr-2'}>{props.title}</div>
29− <div className={'text-600'}>{props.currentSelectionTitle}</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>
30 </div>
81 </div>
31− <div className={'white bg-blue s-10 flex-center-all text-sm'}>{'▼'}</div>
32− </div>
33− </button>
82+ </button>
34
83
35− <div className={`abs flex-column bg-white t-12 l-0 w-100 ${menuVisibility}`}>
84+ <div className={`abs flex-column bg-white t-12 l-0 w-100 z-1 ${menuVisibility}`}>
36− <div className={'pv-1 b-solid b-gray-50 b-2 bb-0'} />
85+ <div className={'pv-1 b-solid b-gray-50 b-2 bb-0'} />
37
86
38− { props.children }
87+ { optionElems }
39
88
40− <div className={'pv-1 b-solid b-gray-50 b-2 bt-0'} />
89+ <div className={'pv-1 b-solid b-gray-50 b-2 bt-0'} />
41− </div>
90+ </div>
42
91
43− </div>
92+ </div>
44− )
93+ )
94+ }
45}
95}
46
96
47InnerLabelDropdown.propTypes = {
97InnerLabelDropdown.propTypes = {
48 disabled: PropTypes.bool,
98 disabled: PropTypes.bool,
49− isOpen: PropTypes.bool,
50− handleToggle: PropTypes.func,
51− handleClose: PropTypes.func,
52 className: PropTypes.string,
99 className: PropTypes.string,
53 title: PropTypes.string,
100 title: PropTypes.string,
54− children: PropTypes.node,
55 style: PropTypes.object,
101 style: PropTypes.object,
56 currentSelectionTitle: PropTypes.string,
102 currentSelectionTitle: PropTypes.string,
103+ children: PropTypes.node,
57};
104};
58
105
59−
60InnerLabelDropdown.defaultProps = {
106InnerLabelDropdown.defaultProps = {
61− isOpen: false,
107+ disabled: false,
62 className: '',
108 className: '',
63 title: '',
109 title: '',
64− currentSelectionTitle: '',
65− handleToggle: () => {},
66− handleClose: () => {},
67− disabled: false,
68 style: {},
110 style: {},
111+ currentSelectionTitle: '',
69};
112};
70
113
71export default InnerLabelDropdown
114export default InnerLabelDropdown
src/bridge/components/ReactSVGComponents.js+24−24
@@ -2,59 +2,59 @@ import React from 'react'
2import { get, map } from 'lodash'
2import { get, map } from 'lodash'
3
3
4const ReactSVGComponents = {
4const ReactSVGComponents = {
5− svg: p => {
5+ svg: (p, key) => {
6 return (
6 return (
7− <svg {...p.attr} version={'1.1'} xmlns={'http://www.w3.org/2000/svg'}>
7+ <svg {...p.attr} version={'1.1'} xmlns={'http://www.w3.org/2000/svg'} key={key}>
8− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
8+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
9 </svg>
9 </svg>
10 )
10 )
11 },
11 },
12− circle: p => {
12+ circle: (p, key) => {
13 return (
13 return (
14− <circle {...p.attr}>
14+ <circle {...p.attr} key={key}>
15− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
15+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
16 </circle>
16 </circle>
17 )
17 )
18 },
18 },
19− rect: p => {
19+ rect: (p, key) => {
20 return (
20 return (
21− <rect {...p.attr}>
21+ <rect {...p.attr} key={key}>
22− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
22+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
23 </rect>
23 </rect>
24 )
24 )
25 },
25 },
26− path: p => {
26+ path: (p, key) => {
27 return (
27 return (
28− <path {...p.attr}>
28+ <path {...p.attr} key={key}>
29− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
29+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
30 </path>
30 </path>
31 )
31 )
32 },
32 },
33− g: p => {
33+ g: (p, key) => {
34 return (
34 return (
35− <g {...p.attr}>
35+ <g {...p.attr} key={key}>
36− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
36+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
37 </g>
37 </g>
38 )
38 )
39 },
39 },
40− polygon: p => {
40+ polygon: (p, key) => {
41 return (
41 return (
42− <polygon {...p.attr}>
42+ <polygon {...p.attr} key={key}>
43− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
43+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
44 </polygon>
44 </polygon>
45 )
45 )
46 },
46 },
47− line: p => {
47+ line: (p, key) => {
48 return (
48 return (
49− <line {...p.attr}>
49+ <line {...p.attr} key={key}>
50− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
50+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
51 </line>
51 </line>
52 )
52 )
53 },
53 },
54− polyline: p => {
54+ polyline: (p, key) => {
55 return (
55 return (
56− <polyline {...p.attr}>
56+ <polyline {...p.attr} key={key}>
57− { map(get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
57+ { map(get(p, 'children', []), (child, idx) => ReactSVGComponents[child.tag](child, idx)) }
58 </polyline>
58 </polyline>
59 )
59 )
60 }
60 }
src/bridge/components/StatelessTransaction.js+99−23
@@ -3,7 +3,8 @@ import React from 'react'
3
3
4import { Code, H3 } from './Base'
4import { Code, H3 } from './Base'
5import { Button } from './Base'
5import { Button } from './Base'
6−import { CheckboxButton, Input, InnerLabel } from './Base'
6+import { CheckboxButton, Input, InnerLabel,
7+ InnerLabelDropdown } from './Base'
7import { Warning } from '../components/Base'
8import { Warning } from '../components/Base'
8
9
9import { addressFromSecp256k1Public } from '../lib/wallet'
10import { addressFromSecp256k1Public } from '../lib/wallet'
@@ -27,11 +28,12 @@ class StatelessTransaction extends React.Component {
27 gasLimit: '600000',
28 gasLimit: '600000',
28 showGasDetails: false,
29 showGasDetails: false,
29 userApproval: false,
30 userApproval: false,
30− chainId: '',
31+ chainId: '1',
32+ customChain: false,
31− nonce: '',
33+ nonce: '0',
32 stx: Nothing(),
34 stx: Nothing(),
33 txn: Nothing(),
35 txn: Nothing(),
34− txError: Nothing(),
36+ txError: Nothing()
35 }
37 }
36
38
37 this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
39 this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
@@ -46,6 +48,7 @@ class StatelessTransaction extends React.Component {
46 this.setGasLimit = this.setGasLimit.bind(this)
48 this.setGasLimit = this.setGasLimit.bind(this)
47 this.rangeChange = this.rangeChange.bind(this)
49 this.rangeChange = this.rangeChange.bind(this)
48 this.toggleGasDetails = this.toggleGasDetails.bind(this)
50 this.toggleGasDetails = this.toggleGasDetails.bind(this)
51+ this.handleChainUpdate = this.handleChainUpdate.bind(this)
49 }
52 }
50
53
51 componentDidMount() {
54 componentDidMount() {
@@ -85,17 +88,20 @@ class StatelessTransaction extends React.Component {
85
88
86 submit(){
89 submit(){
87 const { props, state } = this
90 const { props, state } = this
88− sendSignedTransaction(props.web3.value, state.stx)
91+ sendSignedTransaction(props.web3.value, state.stx, props.setTxnConfirmations)
89 .then(sent => {
92 .then(sent => {
90 props.setTxnHashCursor(sent)
93 props.setTxnHashCursor(sent)
91 props.popRoute()
94 props.popRoute()
92
95
96+ let routeData = {};
93 if (props.networkSeed) {
97 if (props.networkSeed) {
94− props.setNetworkSeedCache(props.networkSeed)
98+ props.setNetworkSeedCache(props.networkSeed);
95− props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION, {promptKeyfile: true})
99+ routeData.promptKeyfile = true;
96− } else {
100+ }
97− props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION)
101+ if (props.promptText) {
102+ routeData.promptText = props.promptText;
98 }
103 }
104+ props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION, routeData);
99 })
105 })
100 .catch(err => {
106 .catch(err => {
101 if (err.map) {
107 if (err.map) {
@@ -106,6 +112,21 @@ class StatelessTransaction extends React.Component {
106 })
112 })
107 }
113 }
108
114
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+
109 setUserApproval(){
130 setUserApproval(){
110 const {state} = this
131 const {state} = this
111 this.setState({ userApproval: !state.userApproval })
132 this.setState({ userApproval: !state.userApproval })
@@ -185,13 +206,53 @@ class StatelessTransaction extends React.Component {
185 this.submit()
206 this.submit()
186 }
207 }
187
208
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+
188 render() {
248 render() {
189 const { web3, canGenerate } = this.props
249 const { web3, canGenerate } = this.props
190 const { gasPrice, gasLimit, nonce, chainId,
250 const { gasPrice, gasLimit, nonce, chainId,
191− txn, stx, userApproval, showGasDetails } = this.state
251+ txn, stx, userApproval, showGasDetails,
252+ customChain } = this.state
192
253
193 const { setNonce, setChainId, setGasLimit, setGasPrice, toggleGasDetails,
254 const { setNonce, setChainId, setGasLimit, setGasPrice, toggleGasDetails,
194− setUserApproval, sendTxn, createUnsignedTxn } = this
255+ setUserApproval, sendTxn, createUnsignedTxn, handleChainUpdate } = this
195
256
196 const { state } = this
257 const { state } = this
197
258
@@ -199,6 +260,8 @@ class StatelessTransaction extends React.Component {
199 const canApprove = Just.hasInstance(stx)
260 const canApprove = Just.hasInstance(stx)
200 const canSend = Just.hasInstance(stx) && userApproval === true
261 const canSend = Just.hasInstance(stx) && userApproval === true
201
262
263+ const chainOptions = this.getChainOptions()
264+
202 const generateButtonColor =
265 const generateButtonColor =
203 Nothing.hasInstance(txn)
266 Nothing.hasInstance(txn)
204 ? 'blue'
267 ? 'blue'
@@ -294,7 +357,7 @@ class StatelessTransaction extends React.Component {
294
357
295 const nonceDialogue =
358 const nonceDialogue =
296 <Input
359 <Input
297− className={ 'mono mt-4' }
360+ className={ 'mono mt-4 mb-4' }
298 prop-size={ 'md' }
361 prop-size={ 'md' }
299 prop-format={ 'innerLabel' }
362 prop-format={ 'innerLabel' }
300 value={ nonce }
363 value={ nonce }
@@ -305,18 +368,30 @@ class StatelessTransaction extends React.Component {
305 </InnerLabel>
368 </InnerLabel>
306 </Input>
369 </Input>
307
370
371+ const chainDialogueTitle = customChain ? "Custom" : this.getChainTitle(chainId)
308 const chainDialogue =
372 const chainDialogue =
309− <Input
373+ <InnerLabelDropdown
310− className={ 'mono mt-4 mb-8' }
374+ className={'mt-6'}
311− prop-size={ 'md' }
375+ fullWidth={true}
312− prop-format={ 'innerLabel' }
376+ title={'Chain ID'}
313− value={ chainId }
377+ options={chainOptions}
314− onChange={ setChainId }
378+ handleUpdate={handleChainUpdate}
379+ currentSelectionTitle={chainDialogueTitle}
315 >
380 >
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+ >
316− <InnerLabel>
391+ <InnerLabel>
317− { 'Chain ID' }
392+ { 'Chain ID' }
318− </InnerLabel>
393+ </InnerLabel>
319− </Input>
394+ </Input>
320
395
321 const onlineParamsDialogue = web3.matchWith({
396 const onlineParamsDialogue = web3.matchWith({
322 Just: _ => <div />,
397 Just: _ => <div />,
@@ -324,6 +399,7 @@ class StatelessTransaction extends React.Component {
324 <React.Fragment>
399 <React.Fragment>
325 { nonceDialogue }
400 { nonceDialogue }
326 { chainDialogue }
401 { chainDialogue }
402+ { customChainDialogue }
327 </React.Fragment>
403 </React.Fragment>
328 })
404 })
329
405
@@ -358,7 +434,7 @@ class StatelessTransaction extends React.Component {
358 onClick={ setUserApproval }
434 onClick={ setUserApproval }
359 state={ userApproval }
435 state={ userApproval }
360 >
436 >
361− <div>
437+ <div onClick={ setUserApproval }>
362 { `I approve this transaction and wish to send.` }
438 { `I approve this transaction and wish to send.` }
363 </div>
439 </div>
364 </CheckboxButton>
440 </CheckboxButton>
src/bridge/lib/contracts.js+4−1
@@ -3,17 +3,20 @@ const CONTRACT_ADDRESSES = {
3 DEV: {
3 DEV: {
4 ecliptic: '0x56db68f29203ff44a803faa2404a44ecbb7a7480',
4 ecliptic: '0x56db68f29203ff44a803faa2404a44ecbb7a7480',
5 azimuth: '0x863d9c2e5c4c133596cfac29d55255f0d0f86381',
5 azimuth: '0x863d9c2e5c4c133596cfac29d55255f0d0f86381',
6− polls: '0x935452c45eda2958976a429c9733c40302995efd'
6+ polls: '0x935452c45eda2958976a429c9733c40302995efd',
7+ delegatedSending: '0xb71c0b6cee1bcae56dfe95cd9d3e41ddd7eafc43'
7 },
8 },
8 ROPSTEN: {
9 ROPSTEN: {
9 ecliptic: '0x8b9f86a28921d9c705b3113a755fb979e1bd1bce',
10 ecliptic: '0x8b9f86a28921d9c705b3113a755fb979e1bd1bce',
10 azimuth: '0x308ab6a6024cf198b57e008d0ac9ad0219886579',
11 azimuth: '0x308ab6a6024cf198b57e008d0ac9ad0219886579',
11 polls: '0xf5DA85f0d285A0F88af2388DD177A221872C8971'
12 polls: '0xf5DA85f0d285A0F88af2388DD177A221872C8971'
13+ //TODO delegatedSending
12 },
14 },
13 MAINNET: {
15 MAINNET: {
14 ecliptic: '0x6ac07b7c4601b5ce11de8dfe6335b871c7c4dd4d',
16 ecliptic: '0x6ac07b7c4601b5ce11de8dfe6335b871c7c4dd4d',
15 azimuth: '0x223c067f8cf28ae173ee5cafea60ca44c335fecb',
17 azimuth: '0x223c067f8cf28ae173ee5cafea60ca44c335fecb',
16 polls: '0x7fecab617c868bb5996d99d95200d2fa708218e4'
18 polls: '0x7fecab617c868bb5996d99d95200d2fa708218e4'
19+ //TODO delegatedSending
17 },
20 },
18}
21}
19
22
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'
7import InviteTicket from '../views/InviteTicket'
7import InviteTicket from '../views/InviteTicket'
8import InviteTransactions from '../views/InviteTransactions.js'
8import InviteTransactions from '../views/InviteTransactions.js'
9
9
10+import InvitesSend from '../views/InvitesSend.js'
11+import InvitesManage from '../views/InvitesManage.js'
12+
10import AcceptTransfer from '../views/AcceptTransfer'
13import AcceptTransfer from '../views/AcceptTransfer'
11import CancelTransfer from '../views/CancelTransfer'
14import CancelTransfer from '../views/CancelTransfer'
12import CreateGalaxy from '../views/CreateGalaxy'
15import CreateGalaxy from '../views/CreateGalaxy'
@@ -42,6 +45,8 @@ const ROUTE_NAMES = {
42 //
45 //
43 INVITE_TICKET: Symbol('INVITE_TICKET'),
46 INVITE_TICKET: Symbol('INVITE_TICKET'),
44 INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
47 INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
48+ INVITES_SEND: Symbol('INVITES_SEND'),
49+ INVITES_MANAGE: Symbol('INVITES_MANAGE'),
45 //
50 //
46 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
51 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
47 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
52 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
@@ -75,6 +80,8 @@ const createRoutes = () => {
75 //
80 //
76 routes[ROUTE_NAMES.INVITE_TICKET] = InviteTicket
81 routes[ROUTE_NAMES.INVITE_TICKET] = InviteTicket
77 routes[ROUTE_NAMES.INVITE_TRANSACTIONS] = InviteTransactions
82 routes[ROUTE_NAMES.INVITE_TRANSACTIONS] = InviteTransactions
83+ routes[ROUTE_NAMES.INVITES_SEND] = InvitesSend
84+ routes[ROUTE_NAMES.INVITES_MANAGE] = InvitesManage
78 //
85 //
79 routes[ROUTE_NAMES.ACCEPT_TRANSFER] = AcceptTransfer
86 routes[ROUTE_NAMES.ACCEPT_TRANSFER] = AcceptTransfer
80 routes[ROUTE_NAMES.CANCEL_TRANSFER] = CancelTransfer
87 routes[ROUTE_NAMES.CANCEL_TRANSFER] = CancelTransfer
@@ -117,6 +124,12 @@ const renderRoute = (props, route) => {
117 : route === ROUTE_NAMES.INVITE_TRANSACTIONS
124 : route === ROUTE_NAMES.INVITE_TRANSACTIONS
118 ? 'Setting up new wallet'
125 ? 'Setting up new wallet'
119
126
127+ : route === ROUTE_NAMES.INVITES_SEND
128+ ? 'Send invites'
129+
130+ : route === ROUTE_NAMES.INVITES_MANAGE
131+ ? 'Manage invites'
132+
120 : route === ROUTE_NAMES.NETWORK
133 : route === ROUTE_NAMES.NETWORK
121 ? `${renderNetworkType(networkType)}`
134 ? `${renderNetworkType(networkType)}`
122
135
src/bridge/lib/txn.js+6−6
@@ -3,6 +3,7 @@ import { Just } from 'folktale/maybe'
3import { Ok, Error } from 'folktale/result'
3import { Ok, Error } from 'folktale/result'
4import Tx from 'ethereumjs-tx'
4import Tx from 'ethereumjs-tx'
5import Web3 from 'web3'
5import Web3 from 'web3'
6+import { toWei, fromWei, toHex } from 'web3-utils'
6
7
7import { BRIDGE_ERROR } from '../lib/error'
8import { BRIDGE_ERROR } from '../lib/error'
8import { NETWORK_NAMES } from '../lib/network'
9import { NETWORK_NAMES } from '../lib/network'
@@ -137,7 +138,7 @@ const signTransaction = async config => {
137 setStx(Just(stx))
138 setStx(Just(stx))
138}
139}
139
140
140−const sendSignedTransaction = (web3, stx) => {
141+const sendSignedTransaction = (web3, stx, confirmationCb) => {
141 const txn = stx.matchWith({
142 const txn = stx.matchWith({
142 Just: (tx) => tx.value,
143 Just: (tx) => tx.value,
143 Nothing: () => {
144 Nothing: () => {
@@ -155,6 +156,10 @@ const sendSignedTransaction = (web3, stx) => {
155 .on('receipt', txn => {
156 .on('receipt', txn => {
156 resolve(Just(Ok(txn.transactionHash)))
157 resolve(Just(Ok(txn.transactionHash)))
157 })
158 })
159+ .on('confirmation', (confirmationNum, txn) => {
160+ confirmationCb(txn.transactionHash, confirmationNum + 1)
161+ resolve(Just(Ok(txn.transactionHash)))
162+ })
158 .on('error', err => {
163 .on('error', err => {
159 reject(Just(Error(err.message)))
164 reject(Just(Error(err.message)))
160 })
165 })
@@ -203,11 +208,6 @@ const getTxnInfo = async (web3, addr) => {
203 }
208 }
204}
209}
205
210
206−const dummy = new Web3()
207−const toHex = dummy.utils.toHex
208−const toWei = dummy.utils.toWei
209−const fromWei = dummy.utils.fromWei
210−
211const canDecodePatp = p => {
211const canDecodePatp = p => {
212 try {
212 try {
213 ob.patp2dec(p)
213 ob.patp2dec(p)
src/bridge/lib/wallet.js+2−2
@@ -6,6 +6,7 @@ import Maybe from 'folktale/maybe'
6import * as secp256k1 from 'secp256k1'
6import * as secp256k1 from 'secp256k1'
7import * as ob from 'urbit-ob'
7import * as ob from 'urbit-ob'
8import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
8import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
9+import { isAddress } from 'web3-utils'
9
10
10const DEFAULT_HD_PATH = "m/44'/60'/0'/0/0"
11const DEFAULT_HD_PATH = "m/44'/60'/0'/0/0"
11
12
@@ -71,8 +72,7 @@ const stripHexPrefix = hex =>
71const keccak256 = str =>
72const keccak256 = str =>
72 keccak('keccak256').update(str).digest()
73 keccak('keccak256').update(str).digest()
73
74
74−const isValidAddress = (address) =>
75+const isValidAddress = isAddress
75− /^0x[0-9a-fA-F]{40}$/.test(address)
76
76
77const toChecksumAddress = (address) => {
77const toChecksumAddress = (address) => {
78 const addr = stripHexPrefix(address).toLowerCase()
78 const addr = stripHexPrefix(address).toLowerCase()
src/bridge/style/extend.css+8
@@ -494,6 +494,14 @@ code {
494 flex-wrap: wrap;
494 flex-wrap: wrap;
495}
495}
496
496
497+.full-width {
498+ width: 100%;
499+}
500+
501+.z-1 {
502+ z-index: 1
503+}
504+
497/**
505/**
498 Passport styles
506 Passport styles
499**/
507**/
src/bridge/views/AcceptTransfer.js+1
@@ -126,6 +126,7 @@ class AcceptTransfer extends React.Component {
126 walletHdPath={props.walletHdPath}
126 walletHdPath={props.walletHdPath}
127 networkType={props.networkType}
127 networkType={props.networkType}
128 setTxnHashCursor={props.setTxnHashCursor}
128 setTxnHashCursor={props.setTxnHashCursor}
129+ setTxnConfirmations={props.setTxnConfirmations}
129 popRoute={props.popRoute}
130 popRoute={props.popRoute}
130 pushRoute={props.pushRoute}
131 pushRoute={props.pushRoute}
131 // Other
132 // Other
src/bridge/views/CancelTransfer.js+1
@@ -87,6 +87,7 @@ class CancelTransfer extends React.Component {
87 walletHdPath={props.walletHdPath}
87 walletHdPath={props.walletHdPath}
88 networkType={props.networkType}
88 networkType={props.networkType}
89 setTxnHashCursor={props.setTxnHashCursor}
89 setTxnHashCursor={props.setTxnHashCursor}
90+ setTxnConfirmations={props.setTxnConfirmations}
90 popRoute={props.popRoute}
91 popRoute={props.popRoute}
91 pushRoute={props.pushRoute}
92 pushRoute={props.pushRoute}
92 // Other
93 // Other
src/bridge/views/CreateGalaxy.js+2−1
@@ -167,7 +167,7 @@ class CreateGalaxy extends React.Component {
167 <ValidatedSigil
167 <ValidatedSigil
168 className='tr-0 mt-05 mr-0 abs'
168 className='tr-0 mt-05 mr-0 abs'
169 patp={state.galaxyName}
169 patp={state.galaxyName}
170− size={76}
170+ size={68}
171 margin={8} />
171 margin={8} />
172 </GalaxyInput>
172 </GalaxyInput>
173
173
@@ -212,6 +212,7 @@ class CreateGalaxy extends React.Component {
212 walletHdPath={props.walletHdPath}
212 walletHdPath={props.walletHdPath}
213 networkType={props.networkType}
213 networkType={props.networkType}
214 setTxnHashCursor={props.setTxnHashCursor}
214 setTxnHashCursor={props.setTxnHashCursor}
215+ setTxnConfirmations={props.setTxnConfirmations}
215 popRoute={props.popRoute}
216 popRoute={props.popRoute}
216 pushRoute={props.pushRoute}
217 pushRoute={props.pushRoute}
217 // Other
218 // 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 {
253 <ValidatedSigil
253 <ValidatedSigil
254 className={'tr-0 mt-05 mr-0 abs'}
254 className={'tr-0 mt-05 mr-0 abs'}
255 patp={state.desiredPoint}
255 patp={state.desiredPoint}
256− size={76}
256+ size={68}
257 margin={8}
257 margin={8}
258 validator={() => this.validatePoint(state.desiredPoint)} />
258 validator={() => this.validatePoint(state.desiredPoint)} />
259 </PointInput>
259 </PointInput>
@@ -303,6 +303,7 @@ class IssueChild extends React.Component {
303 walletHdPath={props.walletHdPath}
303 walletHdPath={props.walletHdPath}
304 networkType={props.networkType}
304 networkType={props.networkType}
305 setTxnHashCursor={props.setTxnHashCursor}
305 setTxnHashCursor={props.setTxnHashCursor}
306+ setTxnConfirmations={props.setTxnConfirmations}
306 popRoute={props.popRoute}
307 popRoute={props.popRoute}
307 pushRoute={props.pushRoute}
308 pushRoute={props.pushRoute}
308 // Other
309 // Other
src/bridge/views/Network.js+47−99
@@ -1,11 +1,7 @@
1import Maybe from 'folktale/maybe'
1import Maybe from 'folktale/maybe'
2import React from 'react'
2import React from 'react'
3import { Button } from '../components/Base'
3import { Button } from '../components/Base'
4−import {
5− DropdownItem,
6− InnerLabelDropdown,
4+import { InnerLabelDropdown } from '../components/Base'
7− DropdownDivider,
8−} from '../components/Base'
9import { Row, Col, H1, P } from '../components/Base'
5import { Row, Col, H1, P } from '../components/Base'
10import * as azimuth from 'azimuth-js'
6import * as azimuth from 'azimuth-js'
11import Web3 from 'web3'
7import Web3 from 'web3'
@@ -19,21 +15,9 @@ class Network extends React.Component {
19 constructor(props) {
15 constructor(props) {
20 super(props)
16 super(props)
21
17
22− this.state = {
23− dropdownOpen: false
24− };
25−
26− this.toggle = this.toggle.bind(this);
27− this.close = this.close.bind(this);
28 this.handleNetworkChange = this.handleNetworkChange.bind(this);
18 this.handleNetworkChange = this.handleNetworkChange.bind(this);
29 }
19 }
30
20
31− toggle() {
32− this.setState((state, _) => ({
33− dropdownOpen: !state.dropdownOpen
34− }))
35− }
36−
37 handleNetworkChange(network) {
21 handleNetworkChange(network) {
38 const { setNetworkType, setNetwork } = this.props
22 const { setNetworkType, setNetwork } = this.props
39
23
@@ -86,7 +70,10 @@ class Network extends React.Component {
86 // We may want to offer the ability to select a target network for
70 // We may want to offer the ability to select a target network for
87 // transactions when offline.
71 // transactions when offline.
88
72
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")
89− const web3 = new Web3()
76+ const web3 = new Web3(provider)
90
77
91 const target =
78 const target =
92 process.env.NODE_ENV === 'development'
79 process.env.NODE_ENV === 'development'
@@ -99,97 +86,58 @@ class Network extends React.Component {
99 }
86 }
100 }
87 }
101
88
102− selectClose(f) {
89+ getNetworkOptions() {
103− f()
90+ return [{
104− this.setState({ dropdownOpen: false })
91+ title: 'Main Network (default)',
92+ value: NETWORK_NAMES.MAINNET
93+ }, {
94+ title: 'Local Node',
95+ value: NETWORK_NAMES.LOCAL
105− }
96+ }, {
106−
97+ title: 'Ropsten',
107−
98+ value: NETWORK_NAMES.ROPSTEN
108− close () {
99+ }, {
109− this.setState({ dropdownOpen: false })
100+ type: 'divider'
101+ }, {
102+ title: 'Offline',
103+ value: NETWORK_NAMES.OFFLINE
104+ }]
110 }
105 }
111
106
112 render() {
107 render() {
113− const { dropdownOpen } = this.state
114 const { networkType, pushRoute } = this.props
108 const { networkType, pushRoute } = this.props
109+ const networkOptions = this.getNetworkOptions()
115
110
116 return (
111 return (
117 <Row>
112 <Row>
118 <Col>
113 <Col>
119 <H1>{ 'Select Network' }</H1>
114 <H1>{ 'Select Network' }</H1>
120
115
121−
122− <P>
116+ <P>
123− {
117+ {
124− "Please select the Ethereum node you'd like to send " +
118+ "Please select the Ethereum node you'd like to send " +
125− "transactions to. For highly valuable keys, please select " +
119+ "transactions to. For highly valuable keys, please select " +
126− "offline mode."
120+ "offline mode."
127− }
121+ }
128− </P>
122+ </P>
129−
123+
130−
131− <InnerLabelDropdown
124+ <InnerLabelDropdown
132− className={'mt-6'}
125+ className={'mt-6'}
133− handleToggle={this.toggle}
134− handleClose={this.close}
135− isOpen={dropdownOpen}
136− title={'Node:'}
126+ title={'Node:'}
127+ handleUpdate={this.handleNetworkChange}
128+ options={networkOptions}
137− currentSelectionTitle={renderNetworkType(networkType)}
129+ 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>
130+ </InnerLabelDropdown>
183−
131+
184− <Button
132+ <Button
185− className={'mt-10'}
133+ className={'mt-10'}
186− onClick={ () => pushRoute(ROUTE_NAMES.WALLET) }
134+ onClick={ () => pushRoute(ROUTE_NAMES.WALLET) }
187− >
135+ >
188− { 'Continue →' }
136+ { 'Continue →' }
189− </Button>
137+ </Button>
190−
138+
191− </Col>
139+ </Col>
192− </Row>
140+ </Row>
193 )
141 )
194 }
142 }
195}
143}
src/bridge/views/Point.js+5−5
@@ -1,4 +1,4 @@
1−import Maybe from 'folktale/maybe'
1+import { Just, Nothing } from 'folktale/maybe'
2import React from 'react'
2import React from 'react'
3import { pour } from 'sigil-js'
3import { pour } from 'sigil-js'
4import * as ob from 'urbit-ob'
4import * as ob from 'urbit-ob'
@@ -85,8 +85,8 @@ class Point extends React.Component {
85
85
86 const pointDetails =
86 const pointDetails =
87 point in pointCache
87 point in pointCache
88− ? Maybe.Just(pointCache[point])
88+ ? Just(pointCache[point])
89− : Maybe.Nothing()
89+ : Nothing()
90
90
91 const name = ob.patp(point)
91 const name = ob.patp(point)
92
92
@@ -96,9 +96,9 @@ class Point extends React.Component {
96 size: 256
96 size: 256
97 })
97 })
98
98
99− const online = Maybe.Just.hasInstance(web3)
99+ const online = Just.hasInstance(web3)
100
100
101− const authenticated = Maybe.Just.hasInstance(wallet)
101+ const authenticated = Just.hasInstance(wallet)
102
102
103 const routeHandler = route => {
103 const routeHandler = route => {
104 popRoute()
104 popRoute()
src/bridge/views/Point/Actions.js+56−23
@@ -9,10 +9,12 @@ import { BRIDGE_ERROR } from '../../lib/error'
9import { Row, Col, H2, P } from '../../components/Base'
9import { Row, Col, H2, P } from '../../components/Base'
10import { Button } from '../../components/Base'
10import { Button } from '../../components/Base'
11import { ROUTE_NAMES } from '../../lib/router'
11import { ROUTE_NAMES } from '../../lib/router'
12−
12+import { azimuth } from 'azimuth-js'
13
13
14const isPlanet = point =>
14const 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);
16
18
17const Actions = (props) => {
19const Actions = (props) => {
18 const { pushRoute, online, point, pointDetails, wallet } = props
20 const { pushRoute, online, point, pointDetails, wallet } = props
@@ -24,35 +26,30 @@ const Actions = (props) => {
24 }
26 }
25 })
27 })
26
28
27− const canSetSpawnProxy = pointDetails.matchWith({
29+ const isOwner = pointDetails.matchWith({
28 Nothing: _ => false,
30 Nothing: _ => false,
29− Just: details =>
30− eqAddr(details.value.owner, addr) && details.value.active
31+ Just: details => eqAddr(details.value.owner, addr)
31− })
32+ });
32−
33− const canSetManagementProxy = pointDetails.matchWith({
33+ const isActive = pointDetails.matchWith({
34 Nothing: _ => false,
34 Nothing: _ => false,
35− Just: details =>
35+ Just: details => details.value.active
36− eqAddr(details.value.owner, addr) && details.value.active
37− })
36+ });
37+ const isActiveOwner = (isOwner && isActive);
38+ const canSetSpawnProxy = isActiveOwner;
39+ const canSetManagementProxy = isActiveOwner;
38
40
39− const canConfigureKeys = pointDetails.matchWith({
41+ const canManage = (isOwner || pointDetails.matchWith({
40 Nothing: _ => false,
42 Nothing: _ => false,
41− Just: details => {
43+ Just: details => eqAddr(details.value.managementProxy, addr)
42− const canManage =
44+ }));
43− eqAddr(details.value.owner, addr)
45+ const canConfigureKeys = (canManage && isActive);
44− || eqAddr(details.value.managementProxy, addr)
45−
46− return canManage && details.value.active
47− }
48− })
49
46
50 const canIssueChild =
47 const canIssueChild =
51 pointDetails.matchWith({
48 pointDetails.matchWith({
52 Nothing: () => false,
49 Nothing: () => false,
53 Just: (details) => {
50 Just: (details) => {
54 const hasPermission =
51 const hasPermission =
55− eqAddr(details.value.owner, addr)
52+ isOwner
56 || eqAddr(details.value.spawnProxy, addr)
53 || eqAddr(details.value.spawnProxy, addr)
57
54
58 const isBooted =
55 const isBooted =
@@ -65,6 +62,7 @@ const Actions = (props) => {
65 })
62 })
66
63
67 const planet = isPlanet(point)
64 const planet = isPlanet(point)
65+ const star = isStar(point)
68
66
69 const canTransfer =
67 const canTransfer =
70 pointDetails.matchWith({
68 pointDetails.matchWith({
@@ -112,6 +110,36 @@ const Actions = (props) => {
112 }
110 }
113 })
111 })
114
112
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+
115 return (
143 return (
116 <div>
144 <div>
117 <H2>{ 'Actions' }</H2>
145 <H2>{ 'Actions' }</H2>
@@ -137,7 +165,7 @@ const Actions = (props) => {
137 <Button
165 <Button
138 prop-size={'sm'}
166 prop-size={'sm'}
139 prop-type={'link'}
167 prop-type={'link'}
140− disabled={ !canAcceptTransfer }
168+ disabled={ online && !canAcceptTransfer }
141 onClick={ () => {
169 onClick={ () => {
142 pushRoute(ROUTE_NAMES.ACCEPT_TRANSFER)
170 pushRoute(ROUTE_NAMES.ACCEPT_TRANSFER)
143 }}
171 }}
@@ -159,7 +187,7 @@ const Actions = (props) => {
159 <Button
187 <Button
160 prop-size={'sm'}
188 prop-size={'sm'}
161 prop-type={'link'}
189 prop-type={'link'}
162− disabled={ !canGenKeyfile }
190+ disabled={ online && !canGenKeyfile }
163 onClick={ () => {
191 onClick={ () => {
164 pushRoute(ROUTE_NAMES.GEN_KEYFILE)
192 pushRoute(ROUTE_NAMES.GEN_KEYFILE)
165 }}
193 }}
@@ -215,6 +243,11 @@ const Actions = (props) => {
215 </Button>
243 </Button>
216
244
217 </Col>
245 </Col>
246+ <Col className={'flex flex-column items-start col-md-4'}>
247+
248+ { invites }
249+
250+ </Col>
218 </Row>
251 </Row>
219 </div>
252 </div>
220 )
253 )
src/bridge/views/Points.js+5−1
@@ -4,6 +4,7 @@ import { Button } from '../components/Base'
4import * as azimuth from 'azimuth-js'
4import * as azimuth from 'azimuth-js'
5
5
6import PointList from '../components/PointList'
6import PointList from '../components/PointList'
7+import { NETWORK_NAMES } from '../lib/network'
7import { ROUTE_NAMES } from '../lib/router'
8import { ROUTE_NAMES } from '../lib/router'
8import {
9import {
9 ETH_ZERO_ADDR,
10 ETH_ZERO_ADDR,
@@ -21,6 +22,9 @@ class Points extends React.Component {
21 constructor(props) {
22 constructor(props) {
22 super(props)
23 super(props)
23
24
25+ const { networkType } = props
26+ const loading = networkType !== NETWORK_NAMES.OFFLINE
27+
24 this.state = {
28 this.state = {
25 points: [],
29 points: [],
26 incoming: [],
30 incoming: [],
@@ -28,7 +32,7 @@ class Points extends React.Component {
28 voting: [],
32 voting: [],
29 spawning: [],
33 spawning: [],
30 eclipticOwner: false,
34 eclipticOwner: false,
31− loading: true
35+ loading: loading
32 }
36 }
33 }
37 }
34
38
src/bridge/views/SentTransaction.js+104−44
@@ -1,3 +1,4 @@
1+import Maybe from 'folktale/maybe'
1import React from 'react'
2import React from 'react'
2import { Row, Col, H1, H3, P, Warning, Anchor } from '../components/Base'
3import { Row, Col, H1, H3, P, Warning, Anchor } from '../components/Base'
3import { Button } from '../components/Base'
4import { Button } from '../components/Base'
@@ -6,55 +7,100 @@ import { ROUTE_NAMES } from '../lib/router'
6import { BRIDGE_ERROR, renderTxnError } from '../lib/error'
7import { BRIDGE_ERROR, renderTxnError } from '../lib/error'
7import { NETWORK_NAMES } from '../lib/network'
8import { NETWORK_NAMES } from '../lib/network'
8
9
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>
35
10
36− return (
11+class Success extends React.Component {
37− <Row>
12+
38− <Col>
13+ constructor(props) {
39− <H1>{ 'Your Transaction was Sent' }</H1>
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)!`
40
74
75+ return (
76+ <Row>
77+ <Col>
78+ <H1>{ 'Your Transaction was Sent' }</H1>
79+
41− <P>
80+ <P>
42− {
81+ {
43− `We sent your transaction to the chain. It can take some time to
82+ `We sent your transaction to the chain. It can take some time to
44 execute, especially if the network is busy. ${esmessage}`
83 execute, especially if the network is busy. ${esmessage}`
45− }
84+ }
46− </P>
85+ </P>
47
86
48− <H3>{ 'Transaction Hash' }</H3>
87+ <H3>{ 'Transaction Hash' }</H3>
49− <P>
88+ <P>
50− { props.hash }
89+ { hash }
51− </P>
90+ </P>
52
91
92+ <H3>{ 'Transaction Status' }</H3>
93+ <P>
94+ { status }
95+ </P>
96+ <P>
53− { esanchor }
97+ { esanchor }
98+ </P>
54
99
55− </Col>
100+ </Col>
56− </Row>
101+ </Row>
57− )
102+ )
103+ }
58}
104}
59
105
60const Failure = (props) =>
106const Failure = (props) =>
@@ -76,10 +122,11 @@ const Failure = (props) =>
76 </Row>
122 </Row>
77
123
78const SentTransaction = (props) => {
124const SentTransaction = (props) => {
79− const { web3, txnHashCursor, networkType, popRoute, pushRoute } = props
125+ const { web3, txnHashCursor, networkType, popRoute, pushRoute, txnConfirmations} = props
80 const { setPointCursor, pointCursor } = props
126 const { setPointCursor, pointCursor } = props
81
127
82 const promptKeyfile = props.routeData && props.routeData.promptKeyfile
128 const promptKeyfile = props.routeData && props.routeData.promptKeyfile
129+ const promptText = props.routeData && props.routeData.promptText
83
130
84 const w3 = web3.matchWith({
131 const w3 = web3.matchWith({
85 Nothing: _ => { throw BRIDGE_ERROR.MISSING_WEB3 },
132 Nothing: _ => { throw BRIDGE_ERROR.MISSING_WEB3 },
@@ -97,9 +144,21 @@ const SentTransaction = (props) => {
97 <Success
144 <Success
98 hash={ hash.value }
145 hash={ hash.value }
99 networkType={ networkType }
146 networkType={ networkType }
147+ txnConfirmations={ txnConfirmations }
100 />
148 />
101 })
149 })
102
150
151+ let text;
152+ if (promptText) {
153+ text = (
154+ <Row>
155+ <Col>
156+ <p>{promptText}</p>
157+ </Col>
158+ </Row>
159+ )
160+ }
161+
103 const ok =
162 const ok =
104 <Row>
163 <Row>
105 <Col>
164 <Col>
@@ -140,6 +199,7 @@ const SentTransaction = (props) => {
140 return (
199 return (
141 <div>
200 <div>
142 { body }
201 { body }
202+ { text }
143 { ok }
203 { ok }
144 { keyfile }
204 { keyfile }
145 </div>
205 </div>
src/bridge/views/SetKeys.js+1
@@ -194,6 +194,7 @@ class SetKeys extends React.Component {
194 walletHdPath={props.walletHdPath}
194 walletHdPath={props.walletHdPath}
195 networkType={props.networkType}
195 networkType={props.networkType}
196 setTxnHashCursor={props.setTxnHashCursor}
196 setTxnHashCursor={props.setTxnHashCursor}
197+ setTxnConfirmations={props.setTxnConfirmations}
197 popRoute={props.popRoute}
198 popRoute={props.popRoute}
198 pushRoute={props.pushRoute}
199 pushRoute={props.pushRoute}
199 // Tx
200 // Tx
src/bridge/views/SetProxy.js+1
@@ -162,6 +162,7 @@ class SetProxy extends React.Component {
162 walletHdPath={props.walletHdPath}
162 walletHdPath={props.walletHdPath}
163 networkType={props.networkType}
163 networkType={props.networkType}
164 setTxnHashCursor={props.setTxnHashCursor}
164 setTxnHashCursor={props.setTxnHashCursor}
165+ setTxnConfirmations={props.setTxnConfirmations}
165 popRoute={props.popRoute}
166 popRoute={props.popRoute}
166 pushRoute={props.pushRoute}
167 pushRoute={props.pushRoute}
167 // Other
168 // Other
src/bridge/views/Transfer.js+1
@@ -132,6 +132,7 @@ class Transfer extends React.Component {
132 walletHdPath={props.walletHdPath}
132 walletHdPath={props.walletHdPath}
133 networkType={props.networkType}
133 networkType={props.networkType}
134 setTxnHashCursor={props.setTxnHashCursor}
134 setTxnHashCursor={props.setTxnHashCursor}
135+ setTxnConfirmations={props.setTxnConfirmations}
135 popRoute={props.popRoute}
136 popRoute={props.popRoute}
136 pushRoute={props.pushRoute}
137 pushRoute={props.pushRoute}
137 // Checks
138 // Checks
src/bridge/views/Wallet.js+32−84
@@ -1,52 +1,50 @@
1import Maybe from 'folktale/maybe'
1import Maybe from 'folktale/maybe'
2import React from 'react'
2import React from 'react'
3import { Button, H1, P } from '../components/Base'
3import { Button, H1, P } from '../components/Base'
4−import {
5− InnerLabelDropdown,
4+import { InnerLabelDropdown } from '../components/Base'
6− DropdownItem,
7− DropdownDivider
8−} from '../components/Base'
9import { Row, Col } from '../components/Base'
5import { Row, Col } from '../components/Base'
10
6
11import { ROUTE_NAMES } from '../lib/router'
7import { ROUTE_NAMES } from '../lib/router'
12import { WALLET_NAMES, renderWalletType } from '../lib/wallet'
8import { WALLET_NAMES, renderWalletType } from '../lib/wallet'
13
9
14class Wallet extends React.Component {
10class 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−
28 componentDidMount() {
11 componentDidMount() {
29 const { setWallet } = this.props
12 const { setWallet } = this.props
30 setWallet(Maybe.Nothing())
13 setWallet(Maybe.Nothing())
31 }
14 }
32
15
33− toggle() {
16+ getWalletOptions() {
34− this.setState((state, _) => ({
17+ return [{
35− dropdownOpen: !state.dropdownOpen
18+ title: 'Urbit Master Ticket',
36− }))
19+ value: WALLET_NAMES.TICKET
20+ }, {
21+ title: 'Urbit Master Ticket (Shards)',
22+ value: WALLET_NAMES.SHARDS
23+ }, {
24+ type: 'divider'
37− }
25+ }, {
38−
26+ title: 'Ledger',
39− selectClose(f) {
27+ value: WALLET_NAMES.LEDGER
40− f()
28+ }, {
41− this.setState({ dropdownOpen: false })
29+ title: 'Trezor',
30+ value: WALLET_NAMES.TREZOR
31+ }, {
32+ type: 'divider'
42− }
33+ }, {
43−
34+ title: 'BIP39 Mnemonic',
44− close () {
35+ value: WALLET_NAMES.MNEMONIC
45− this.setState({ dropdownOpen: false })
36+ }, {
37+ title: 'Ethereum Private Key',
38+ value: WALLET_NAMES.PRIVATE_KEY
39+ }, {
40+ title: 'Ethereum Keystore File',
41+ value: WALLET_NAMES.KEYSTORE
42+ }]
46 }
43 }
47
44
48 render() {
45 render() {
49− const { props, state } = this
46+ const { props } = this
47+ const walletOptions = this.getWalletOptions()
50
48
51 return (
49 return (
52 <Row>
50 <Row>
@@ -63,60 +61,10 @@ class Wallet extends React.Component {
63
61
64 <InnerLabelDropdown
62 <InnerLabelDropdown
65 className={'mb-10 mt-6'}
63 className={'mb-10 mt-6'}
66− handleToggle={this.toggle}
67− handleClose={this.close}
68− isOpen={state.dropdownOpen}
69 title={'Wallet Type:'}
64 title={'Wallet Type:'}
65+ options={walletOptions}
66+ handleUpdate={props.setWalletType}
70 currentSelectionTitle={renderWalletType(props.walletType)}>
67 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−
120 </InnerLabelDropdown>
68 </InnerLabelDropdown>
121
69
122 <Button
70 <Button