From 30526da0945619395f6b405221788d04a4de7141 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Thu, 24 Nov 2016 11:46:27 +0100 Subject: [PATCH 01/17] Allow forms, allow target=_blank --- js/src/views/Dapp/dapp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/views/Dapp/dapp.js b/js/src/views/Dapp/dapp.js index 399d7b125..a4be4644e 100644 --- a/js/src/views/Dapp/dapp.js +++ b/js/src/views/Dapp/dapp.js @@ -63,7 +63,7 @@ export default class Dapp extends Component { className={ styles.frame } frameBorder={ 0 } name={ name } - sandbox='allow-same-origin allow-scripts' + sandbox='allow-forms allow-popups allow-same-origin allow-scripts' scrolling='auto' src={ src }> From 6b9b31ad37468a1edeb8ee229094e3c2861e0e30 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Thu, 24 Nov 2016 15:46:25 +0100 Subject: [PATCH 02/17] check for network ID for live/test matching --- js/src/redux/providers/status.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index 2153d1ddf..cedf62d89 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -242,6 +242,7 @@ export default class Status { Promise .all([ this._api.web3.clientVersion(), + this._api.net.version(), this._api.parity.defaultExtraData(), this._api.parity.netChain(), this._api.parity.netPort(), @@ -249,9 +250,11 @@ export default class Status { newConnection ? Promise.resolve(null) : this._api.parity.enode() ]) .then(([ - clientVersion, defaultExtraData, netChain, netPort, rpcSettings, enode + clientVersion, netVersion, defaultExtraData, netChain, netPort, rpcSettings, enode ]) => { - const isTest = netChain === 'morden' || netChain === 'ropsten' || netChain === 'testnet'; + const isTest = + netVersion === '2' || // morden + netVersion === '3'; // ropsten const longStatus = { clientVersion, From fa1343e8af3562022fbf1ea2eead11c5a7bf2009 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Thu, 24 Nov 2016 17:17:08 +0100 Subject: [PATCH 03/17] align copy button to input field --- js/src/ui/Form/Input/input.css | 2 +- js/src/ui/Form/Input/input.js | 22 ++-------------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/js/src/ui/Form/Input/input.css b/js/src/ui/Form/Input/input.css index bc7cfb840..b4d535eb9 100644 --- a/js/src/ui/Form/Input/input.css +++ b/js/src/ui/Form/Input/input.css @@ -18,7 +18,7 @@ .container { display: flex; flex-direction: row; - align-items: flex-end; + align-items: baseline; position: relative; } diff --git a/js/src/ui/Form/Input/input.js b/js/src/ui/Form/Input/input.js index 4dee227c6..e48ab63ff 100644 --- a/js/src/ui/Form/Input/input.js +++ b/js/src/ui/Form/Input/input.js @@ -144,35 +144,17 @@ export default class Input extends Component { } renderCopyButton () { - const { allowCopy, label, hint, floatCopy } = this.props; + const { allowCopy, hideUnderline } = this.props; const { value } = this.state; if (!allowCopy) { return null; } - - const style = { - marginBottom: 13 - }; - const text = typeof allowCopy === 'string' ? allowCopy : value; - if (!label) { - style.marginBottom = 2; - } else if (label && !hint) { - style.marginBottom = 4; - } else if (label && hint) { - style.marginBottom = 10; - } - - if (floatCopy) { - style.position = 'absolute'; - style.left = -24; - style.bottom = style.marginBottom; - style.marginBottom = 0; - } + const style = hideUnderline ? {} : { position: 'relative', top: '2px' }; return (
From 686978fa974dfd73cf050705cf7a05b247c7e928 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Fri, 25 Nov 2016 11:36:32 +0000 Subject: [PATCH 04/17] [ci skip] js-precompiled 20161125-113155 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 809e5a07a..62ef5ea11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1263,7 +1263,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#f1de5e5612d8237143b37aebf237a49475c2c4e6" +source = "git+https://github.com/ethcore/js-precompiled.git#7cb42b0c636f76eb478c9270a1e507ac3c3ba434" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index 6d8c37165..12b0cd9c6 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.73", + "version": "0.2.74", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", From 0e2cd1d0803d023173b42e4203cbad5fde7c6ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Fri, 25 Nov 2016 12:59:15 +0100 Subject: [PATCH 05/17] Fixing phrases generated on windows (#3614) --- ethstore/src/random.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/ethstore/src/random.rs b/ethstore/src/random.rs index 1d050e422..954ec500f 100644 --- a/ethstore/src/random.rs +++ b/ethstore/src/random.rs @@ -47,7 +47,7 @@ impl Random for [u8; 32] { pub fn random_phrase(words: usize) -> String { lazy_static! { static ref WORDS: Vec = String::from_utf8_lossy(include_bytes!("../res/wordlist.txt")) - .split("\n") + .lines() .map(|s| s.to_owned()) .collect(); } @@ -55,8 +55,19 @@ pub fn random_phrase(words: usize) -> String { (0..words).map(|_| rng.choose(&WORDS).unwrap()).join(" ") } -#[test] -fn should_produce_right_number_of_words() { - let p = random_phrase(10); - assert_eq!(p.split(" ").count(), 10); -} \ No newline at end of file +#[cfg(test)] +mod tests { + use super::random_phrase; + + #[test] + fn should_produce_right_number_of_words() { + let p = random_phrase(10); + assert_eq!(p.split(" ").count(), 10); + } + + #[test] + fn should_not_include_carriage_return() { + let p = random_phrase(10); + assert!(!p.contains('\r'), "Carriage return should be trimmed."); + } +} From d0312b89ad9109f21167936ce5644696032f6724 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Fri, 25 Nov 2016 13:03:15 +0100 Subject: [PATCH 06/17] Update test, fix number. (#3612) --- ethcore/res/ethereum/tests | 2 +- ethcore/src/json_tests/transaction.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ethcore/res/ethereum/tests b/ethcore/res/ethereum/tests index 9028c4801..e8f4624b7 160000 --- a/ethcore/res/ethereum/tests +++ b/ethcore/res/ethereum/tests @@ -1 +1 @@ -Subproject commit 9028c4801fd39fbb71a9796979182549a24e81c8 +Subproject commit e8f4624b7f1a15c63674eecf577c7ab76c3b16be diff --git a/ethcore/src/json_tests/transaction.rs b/ethcore/src/json_tests/transaction.rs index 438852124..12e82bca2 100644 --- a/ethcore/src/json_tests/transaction.rs +++ b/ethcore/src/json_tests/transaction.rs @@ -34,7 +34,7 @@ fn do_json_test(json_data: &[u8]) -> Vec { Some(x) if x < 1_150_000 => &old_schedule, Some(_) => &new_schedule }; - let allow_network_id_of_one = number.map_or(false, |n| n >= 3_500_000); + let allow_network_id_of_one = number.map_or(false, |n| n >= 2_675_000); let rlp: Vec = test.rlp.into(); let res = UntrustedRlp::new(&rlp) From 9b9bdaa1b1abaa924eeb7735a17e5392b8bc8a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Fri, 25 Nov 2016 13:04:02 +0100 Subject: [PATCH 07/17] Appending logs by default (#3609) --- logger/src/lib.rs | 9 ++++++--- parity/cli/usage.txt | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/logger/src/lib.rs b/logger/src/lib.rs index 7e13f406c..1e1555c7c 100644 --- a/logger/src/lib.rs +++ b/logger/src/lib.rs @@ -26,9 +26,8 @@ extern crate time; #[macro_use] extern crate lazy_static; -use std::{env, thread}; +use std::{env, thread, fs}; use std::sync::Arc; -use std::fs::File; use std::io::Write; use isatty::{stderr_isatty, stdout_isatty}; use env_logger::LogBuilder; @@ -80,9 +79,13 @@ pub fn setup_log(config: &Config) -> Result, String> { let enable_color = config.color && isatty; let logs = Arc::new(RotatingLogger::new(levels)); let logger = logs.clone(); + let mut open_options = fs::OpenOptions::new(); let maybe_file = match config.file.as_ref() { - Some(f) => Some(try!(File::create(f).map_err(|_| format!("Cannot write to log file given: {}", f)))), + Some(f) => Some(try!(open_options + .append(true).create(true).open(f) + .map_err(|_| format!("Cannot write to log file given: {}", f)) + )), None => None, }; diff --git a/parity/cli/usage.txt b/parity/cli/usage.txt index 97cda74ba..fe0824dfe 100644 --- a/parity/cli/usage.txt +++ b/parity/cli/usage.txt @@ -323,7 +323,7 @@ Miscellaneous Options: -l --logging LOGGING Specify the logging level. Must conform to the same format as RUST_LOG. (default: {flag_logging:?}) --log-file FILENAME Specify a filename into which logging should be - directed. (default: {flag_log_file:?}) + appended. (default: {flag_log_file:?}) --no-config Don't load a configuration file. --no-color Don't use terminal color codes in output. (default: {flag_no_color}) -v --version Show information about version. From ffa123c949cc5ac68f2a430df142d1133ae4612c Mon Sep 17 00:00:00 2001 From: arkpar Date: Fri, 25 Nov 2016 15:12:57 +0100 Subject: [PATCH 08/17] Set default tx price to 0.0025 USD --- ethcore/res/ethereum/tests | 2 +- parity/cli/config.full.toml | 2 +- parity/cli/mod.rs | 4 ++-- parity/params.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ethcore/res/ethereum/tests b/ethcore/res/ethereum/tests index e8f4624b7..d509c7593 160000 --- a/ethcore/res/ethereum/tests +++ b/ethcore/res/ethereum/tests @@ -1 +1 @@ -Subproject commit e8f4624b7f1a15c63674eecf577c7ab76c3b16be +Subproject commit d509c75936ec6cbba683ee1916aa0bca436bc376 diff --git a/parity/cli/config.full.toml b/parity/cli/config.full.toml index 841cf5f24..fcd9a9712 100644 --- a/parity/cli/config.full.toml +++ b/parity/cli/config.full.toml @@ -66,7 +66,7 @@ reseal_on_txs = "all" reseal_min_period = 4000 work_queue_size = 20 relay_set = "cheap" -usd_per_tx = "0" +usd_per_tx = "0.0025" usd_per_eth = "auto" price_update_period = "hourly" gas_floor_target = "4700000" diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index cfe4c4d29..93373c383 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -190,7 +190,7 @@ usage! { or |c: &Config| otry!(c.mining).tx_time_limit.clone().map(Some), flag_relay_set: String = "cheap", or |c: &Config| otry!(c.mining).relay_set.clone(), - flag_usd_per_tx: String = "0", + flag_usd_per_tx: String = "0.0025", or |c: &Config| otry!(c.mining).usd_per_tx.clone(), flag_usd_per_eth: String = "auto", or |c: &Config| otry!(c.mining).usd_per_eth.clone(), @@ -568,7 +568,7 @@ mod tests { flag_tx_gas_limit: Some("6283184".into()), flag_tx_time_limit: Some(100u64), flag_relay_set: "cheap".into(), - flag_usd_per_tx: "0".into(), + flag_usd_per_tx: "0.0025".into(), flag_usd_per_eth: "auto".into(), flag_price_update_period: "hourly".into(), flag_gas_floor_target: "4700000".into(), diff --git a/parity/params.rs b/parity/params.rs index 8af70b91d..28233400e 100644 --- a/parity/params.rs +++ b/parity/params.rs @@ -177,7 +177,7 @@ pub enum GasPricerConfig { impl Default for GasPricerConfig { fn default() -> Self { GasPricerConfig::Calibrated { - usd_per_tx: 0f32, + usd_per_tx: 0.0025f32, recalibration_period: Duration::from_secs(3600), } } From 29585466531eb50aeac91e9cd56eaa6015c42e87 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 25 Nov 2016 15:19:14 +0100 Subject: [PATCH 09/17] Use accountsInfo instead of eth_accounts for first check --- js/src/views/Application/application.js | 39 +++---------------- js/src/views/Application/store.js | 51 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 33 deletions(-) create mode 100644 js/src/views/Application/store.js diff --git a/js/src/views/Application/application.js b/js/src/views/Application/application.js index d49aa5d90..8225eb10f 100644 --- a/js/src/views/Application/application.js +++ b/js/src/views/Application/application.js @@ -17,6 +17,7 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +import { observer } from 'mobx-react'; import Connection from '../Connection'; import ParityBar from '../ParityBar'; @@ -25,13 +26,14 @@ import Container from './Container'; import DappContainer from './DappContainer'; import FrameError from './FrameError'; import Status from './Status'; +import Store from './store'; import TabBar from './TabBar'; import styles from './application.css'; const inFrame = window.parent !== window && window.parent.frames.length !== 0; -const showFirstRun = window.localStorage.getItem('showFirstRun') === '1'; +@observer class Application extends Component { static contextTypes = { api: PropTypes.object.isRequired, @@ -46,13 +48,7 @@ class Application extends Component { blockNumber: PropTypes.object } - state = { - showFirstRun: false - } - - componentWillMount () { - this.checkAccounts(); - } + store = new Store(this.context.api); render () { const [root] = (window.location.hash || '').replace('#/', '').split('/'); @@ -75,12 +71,11 @@ class Application extends Component { renderApp () { const { children, pending, netChain, isTest, blockNumber } = this.props; - const { showFirstRun } = this.state; return ( + showFirstRun={ this.store.firstrunVisible } + onCloseFirstRun={ this.store.closeFirstrun }> ); } - - checkAccounts () { - const { api } = this.context; - - api.eth - .accounts() - .then((accounts) => { - this.setState({ - showFirstRun: showFirstRun || accounts.length === 0 - }); - }) - .catch((error) => { - console.error('checkAccounts', error); - }); - } - - onCloseFirstRun = () => { - window.localStorage.setItem('showFirstRun', '0'); - this.setState({ - showFirstRun: false - }); - } } function mapStateToProps (state) { diff --git a/js/src/views/Application/store.js b/js/src/views/Application/store.js new file mode 100644 index 000000000..4ea0faf3d --- /dev/null +++ b/js/src/views/Application/store.js @@ -0,0 +1,51 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { action, observable } from 'mobx'; + +const showFirstRun = window.localStorage.getItem('showFirstRun') !== '0'; + +export default class Store { + @observable firstrunVisible = showFirstRun; + + constructor (api) { + this._api = api; + + this._checkAccounts(); + } + + @action closeFirstrun = () => { + this.toggleFirstrun(false); + } + + @action toggleFirstrun = (visible = false) => { + this.firstrunVisible = visible; + window.localStorage.setItem('showFirstRun', visible ? '1' : '0'); + } + + _checkAccounts () { + this._api.parity + .accountsInfo() + .then((info) => { + const accounts = Object.keys(info).filter((address) => info[address].uuid); + + this.toggleFirstrun(this.firstrunVisible || !accounts || !accounts.length); + }) + .catch((error) => { + console.error('checkAccounts', error); + }); + } +} From 9b21c96e3aeff5aa2b5998a7a84354ea4c794c6a Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Fri, 25 Nov 2016 15:52:23 +0100 Subject: [PATCH 10/17] Fix Copy to Clipboard Snackbar --- js/src/redux/providers/index.js | 1 + js/src/redux/providers/snackbarActions.js | 34 ++++++++++ js/src/redux/providers/snackbarReducer.js | 44 ++++++++++++ js/src/redux/reducers.js | 5 +- js/src/ui/CopyToClipboard/copyToClipboard.js | 43 ++++++++---- js/src/views/Application/Snackbar/index.js | 17 +++++ js/src/views/Application/Snackbar/snackbar.js | 68 +++++++++++++++++++ js/src/views/Application/application.js | 2 + 8 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 js/src/redux/providers/snackbarActions.js create mode 100644 js/src/redux/providers/snackbarReducer.js create mode 100644 js/src/views/Application/Snackbar/index.js create mode 100644 js/src/views/Application/Snackbar/snackbar.js diff --git a/js/src/redux/providers/index.js b/js/src/redux/providers/index.js index 89064e740..d55b608eb 100644 --- a/js/src/redux/providers/index.js +++ b/js/src/redux/providers/index.js @@ -27,3 +27,4 @@ export signerReducer from './signerReducer'; export statusReducer from './statusReducer'; export blockchainReducer from './blockchainReducer'; export compilerReducer from './compilerReducer'; +export snackbarReducer from './snackbarReducer'; diff --git a/js/src/redux/providers/snackbarActions.js b/js/src/redux/providers/snackbarActions.js new file mode 100644 index 000000000..428958c4c --- /dev/null +++ b/js/src/redux/providers/snackbarActions.js @@ -0,0 +1,34 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export function showSnackbar (message, cooldown) { + return (dispatch, getState) => { + dispatch(openSnackbar(message, cooldown)); + }; +} + +function openSnackbar (message, cooldown) { + return { + type: 'openSnackbar', + message, cooldown + }; +} + +export function closeSnackbar () { + return { + type: 'closeSnackbar' + }; +} diff --git a/js/src/redux/providers/snackbarReducer.js b/js/src/redux/providers/snackbarReducer.js new file mode 100644 index 000000000..7a913f8ca --- /dev/null +++ b/js/src/redux/providers/snackbarReducer.js @@ -0,0 +1,44 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { handleActions } from 'redux-actions'; + +const initialState = { + open: false, + message: '', + cooldown: 1000 +}; + +export default handleActions({ + openSnackbar (state, action) { + const { message, cooldown } = action; + + return { + ...state, + open: true, + cooldown: cooldown || state.cooldown, + message + }; + }, + + closeSnackbar (state) { + return { + ...state, + open: false, + cooldown: initialState.cooldown + }; + } +}, initialState); diff --git a/js/src/redux/reducers.js b/js/src/redux/reducers.js index 1365bdc99..923e32147 100644 --- a/js/src/redux/reducers.js +++ b/js/src/redux/reducers.js @@ -17,7 +17,7 @@ import { combineReducers } from 'redux'; import { routerReducer } from 'react-router-redux'; -import { apiReducer, balancesReducer, blockchainReducer, compilerReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer } from './providers'; +import { apiReducer, balancesReducer, blockchainReducer, compilerReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer, snackbarReducer } from './providers'; import { errorReducer } from '../ui/Errors'; import { settingsReducer } from '../views/Settings'; @@ -37,6 +37,7 @@ export default function () { images: imagesReducer, nodeStatus: nodeStatusReducer, personal: personalReducer, - signer: signerReducer + signer: signerReducer, + snackbar: snackbarReducer }); } diff --git a/js/src/ui/CopyToClipboard/copyToClipboard.js b/js/src/ui/CopyToClipboard/copyToClipboard.js index 568520b09..3ad316435 100644 --- a/js/src/ui/CopyToClipboard/copyToClipboard.js +++ b/js/src/ui/CopyToClipboard/copyToClipboard.js @@ -15,19 +15,25 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + import { IconButton } from 'material-ui'; -import Snackbar from 'material-ui/Snackbar'; import Clipboard from 'react-copy-to-clipboard'; import CopyIcon from 'material-ui/svg-icons/content/content-copy'; import Theme from '../Theme'; -import { darkBlack } from 'material-ui/styles/colors'; + +import { showSnackbar } from '../../redux/providers/snackbarActions'; + const { textColor, disabledTextColor } = Theme.flatButton; import styles from './copyToClipboard.css'; -export default class CopyToClipboard extends Component { +class CopyToClipboard extends Component { static propTypes = { + showSnackbar: PropTypes.func.isRequired, data: PropTypes.string.isRequired, + onCopy: PropTypes.func, size: PropTypes.number, // in px cooldown: PropTypes.number // in ms @@ -42,11 +48,12 @@ export default class CopyToClipboard extends Component { state = { copied: false, - timeout: null + timeoutId: null }; componentWillUnmount () { const { timeoutId } = this.state; + if (timeoutId) { window.clearTimeout(timeoutId); } @@ -59,14 +66,6 @@ export default class CopyToClipboard extends Component { return (
- copied { data } to clipboard
- } - autoHideDuration={ 2000 } - bodyStyle={ { backgroundColor: darkBlack } } - /> { - const { cooldown, onCopy } = this.props; + const { data, onCopy, cooldown, showSnackbar } = this.props; + const message = (
copied { data } to clipboard
); this.setState({ copied: true, - timeout: setTimeout(() => { - this.setState({ copied: false, timeout: null }); + timeoutId: setTimeout(() => { + this.setState({ copied: false, timeoutId: null }); }, cooldown) }); + + showSnackbar(message, cooldown); onCopy(); } } + +function mapDispatchToProps (dispatch) { + return bindActionCreators({ + showSnackbar + }, dispatch); +} + +export default connect( + null, + mapDispatchToProps +)(CopyToClipboard); diff --git a/js/src/views/Application/Snackbar/index.js b/js/src/views/Application/Snackbar/index.js new file mode 100644 index 000000000..0da39f236 --- /dev/null +++ b/js/src/views/Application/Snackbar/index.js @@ -0,0 +1,17 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './snackbar'; diff --git a/js/src/views/Application/Snackbar/snackbar.js b/js/src/views/Application/Snackbar/snackbar.js new file mode 100644 index 000000000..ac6e6b950 --- /dev/null +++ b/js/src/views/Application/Snackbar/snackbar.js @@ -0,0 +1,68 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import { Snackbar as SnackbarMUI } from 'material-ui'; +import { darkBlack } from 'material-ui/styles/colors'; + +import { closeSnackbar } from '../../../redux/providers/snackbarActions'; + +class Snackbar extends Component { + static propTypes = { + closeSnackbar: PropTypes.func.isRequired, + + open: PropTypes.bool, + cooldown: PropTypes.number, + message: PropTypes.any + }; + + render () { + const { open, message, cooldown } = this.props; + + return ( + + ); + } + + handleClose = () => { + this.props.closeSnackbar(); + } +} + +function mapStateToProps (state) { + const { open, message, cooldown } = state.snackbar; + return { open, message, cooldown }; +} + +function mapDispatchToProps (dispatch) { + return bindActionCreators({ + closeSnackbar + }, dispatch); +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Snackbar); diff --git a/js/src/views/Application/application.js b/js/src/views/Application/application.js index d49aa5d90..0d5434534 100644 --- a/js/src/views/Application/application.js +++ b/js/src/views/Application/application.js @@ -21,6 +21,7 @@ import { bindActionCreators } from 'redux'; import Connection from '../Connection'; import ParityBar from '../ParityBar'; +import Snackbar from './Snackbar'; import Container from './Container'; import DappContainer from './DappContainer'; import FrameError from './FrameError'; @@ -87,6 +88,7 @@ class Application extends Component { pending={ pending } /> { children } { blockNumber ? () : null } +
); } From 08c507daaac26b2643545f9203144b930e630c9d Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Fri, 25 Nov 2016 16:46:35 +0100 Subject: [PATCH 11/17] Smarter balance fetching (#3605) * Smarter dApps Manifest fetching... * Fetching only visible accounts (and don't delete other balances) #3590 * Moved balances action into BalancesActions #3590 * Fetch balances for accounts and contracts #3590 * Add balances to contract/address/account views #3590 * Fix transaction not fetching on first load * Remove console.warn * Fix pending tokens not showing #3154 * Fix tokens image update * Remove unused name in Header * Separate Tokens and ETH fetching #3590 * Remove unused isTest * Fetch Tokens Balance via Filter #3590 * Fix linting * Fix updating tokens image (#3590) * Fix contract balances * Improved Status * Fixing secureApi issues... * Fetch all tokens every 2 minutes (for safety) #3590 * PR changes fix * Fix Account error --- js/src/api/format/input.js | 21 +- js/src/api/transport/error.js | 3 +- js/src/api/transport/ws/ws.js | 12 +- js/src/api/util/format.js | 13 + js/src/dapps/tokenreg/Status/actions.js | 2 +- js/src/redux/providers/balances.js | 291 ++++----------- js/src/redux/providers/balancesActions.js | 346 +++++++++++++++++- js/src/redux/providers/balancesReducer.js | 80 +++- js/src/redux/providers/imagesActions.js | 5 +- js/src/redux/providers/imagesReducer.js | 6 +- js/src/redux/providers/personalActions.js | 24 ++ js/src/redux/providers/personalReducer.js | 16 +- js/src/redux/providers/status.js | 37 +- js/src/redux/providers/statusReducer.js | 2 +- js/src/secureApi.js | 8 +- js/src/ui/BlockStatus/blockStatus.js | 2 +- js/src/views/Account/Header/header.js | 43 +-- .../Account/Transactions/transactions.js | 6 + js/src/views/Account/account.js | 37 +- js/src/views/Accounts/List/list.js | 14 +- js/src/views/Accounts/Summary/summary.js | 8 +- js/src/views/Accounts/accounts.js | 30 +- js/src/views/Address/address.js | 36 +- js/src/views/Addresses/addresses.js | 32 +- js/src/views/Contract/contract.js | 30 +- js/src/views/Contracts/contracts.js | 32 +- js/src/views/Dapps/dappsStore.js | 17 + 27 files changed, 798 insertions(+), 355 deletions(-) diff --git a/js/src/api/format/input.js b/js/src/api/format/input.js index 55c85e4f3..80f3bc0eb 100644 --- a/js/src/api/format/input.js +++ b/js/src/api/format/input.js @@ -15,9 +15,9 @@ // along with Parity. If not, see . import BigNumber from 'bignumber.js'; -import { range } from 'lodash'; import { isArray, isHex, isInstanceOf, isString } from '../util/types'; +import { padLeft } from '../util/format'; export function inAddress (address) { // TODO: address validation if we have upper-lower addresses @@ -51,19 +51,20 @@ export function inHash (hash) { return inHex(hash); } -export function pad (input, length) { - const value = inHex(input).substr(2, length * 2); - return '0x' + value + range(length * 2 - value.length).map(() => '0').join(''); -} - export function inTopics (_topics) { let topics = (_topics || []) .filter((topic) => topic === null || topic) - .map((topic) => topic === null ? null : pad(topic, 32)); + .map((topic) => { + if (topic === null) { + return null; + } - // while (topics.length < 4) { - // topics.push(null); - // } + if (Array.isArray(topic)) { + return inTopics(topic); + } + + return padLeft(topic, 32); + }); return topics; } diff --git a/js/src/api/transport/error.js b/js/src/api/transport/error.js index 341839f69..6cb0dac17 100644 --- a/js/src/api/transport/error.js +++ b/js/src/api/transport/error.js @@ -36,7 +36,8 @@ export const ERROR_CODES = { REQUEST_NOT_FOUND: -32042, COMPILATION_ERROR: -32050, ENCRYPTION_ERROR: -32055, - FETCH_ERROR: -32060 + FETCH_ERROR: -32060, + INVALID_PARAMS: -32602 }; export default class TransportError extends ExtendableError { diff --git a/js/src/api/transport/ws/ws.js b/js/src/api/transport/ws/ws.js index c30c910e6..53600b6d3 100644 --- a/js/src/api/transport/ws/ws.js +++ b/js/src/api/transport/ws/ws.js @@ -79,7 +79,7 @@ export default class Ws extends JsonRpcBase { this._ws.onclose = this._onClose; this._ws.onmessage = this._onMessage; - // Get counts in dev mode + // Get counts in dev mode only if (process.env.NODE_ENV === 'development') { this._count = 0; this._lastCount = { @@ -93,8 +93,13 @@ export default class Ws extends JsonRpcBase { const s = Math.round(1000 * n / t) / 1000; if (this._debug) { - console.log('::parityWS', `speed: ${s} req/s`, `count: ${this._count}`); + console.log('::parityWS', `speed: ${s} req/s`, `count: ${this._count}`, `(+${n})`); } + + this._lastCount = { + timestamp: Date.now(), + count: this._count + }; }, 5000); window._parityWS = this; @@ -117,6 +122,7 @@ export default class Ws extends JsonRpcBase { this._connected = false; this._connecting = false; + event.timestamp = Date.now(); this._lastError = event; if (this._autoConnect) { @@ -144,6 +150,8 @@ export default class Ws extends JsonRpcBase { window.setTimeout(() => { if (this._connected) { console.error('ws:onError', event); + + event.timestamp = Date.now(); this._lastError = event; } }, 50); diff --git a/js/src/api/util/format.js b/js/src/api/util/format.js index 93f31a161..f1909748d 100644 --- a/js/src/api/util/format.js +++ b/js/src/api/util/format.js @@ -14,6 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import { range } from 'lodash'; +import { inHex } from '../format/input'; + export function bytesToHex (bytes) { return '0x' + bytes.map((b) => ('0' + b.toString(16)).slice(-2)).join(''); } @@ -33,3 +36,13 @@ export function hex2Ascii (_hex) { export function asciiToHex (string) { return '0x' + string.split('').map((s) => s.charCodeAt(0).toString(16)).join(''); } + +export function padRight (input, length) { + const value = inHex(input).substr(2, length * 2); + return '0x' + value + range(length * 2 - value.length).map(() => '0').join(''); +} + +export function padLeft (input, length) { + const value = inHex(input).substr(2, length * 2); + return '0x' + range(length * 2 - value.length).map(() => '0').join('') + value; +} diff --git a/js/src/dapps/tokenreg/Status/actions.js b/js/src/dapps/tokenreg/Status/actions.js index b07949a28..027de57af 100644 --- a/js/src/dapps/tokenreg/Status/actions.js +++ b/js/src/dapps/tokenreg/Status/actions.js @@ -127,7 +127,7 @@ export const subscribeEvents = () => (dispatch, getState) => { const params = log.params; if (event === 'Registered' && type === 'pending') { - return dispatch(setTokenData(params.id.toNumber(), { + return dispatch(setTokenData(params.id.value.toNumber(), { tla: '...', base: -1, address: params.addr.value, diff --git a/js/src/redux/providers/balances.js b/js/src/redux/providers/balances.js index 9e9c0a481..6b1084934 100644 --- a/js/src/redux/providers/balances.js +++ b/js/src/redux/providers/balances.js @@ -16,68 +16,63 @@ import { throttle } from 'lodash'; -import { getBalances, getTokens } from './balancesActions'; -import { setAddressImage } from './imagesActions'; +import { loadTokens, setTokenReg, fetchBalances, fetchTokens, fetchTokensBalances } from './balancesActions'; +import { padRight } from '../../api/util/format'; import Contracts from '../../contracts'; -import * as abis from '../../contracts/abi'; - -import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png'; - -const ETH = { - name: 'Ethereum', - tag: 'ETH', - image: imagesEthereum -}; export default class Balances { constructor (store, api) { this._api = api; this._store = store; - this._tokens = {}; - this._images = {}; - - this._accountsInfo = null; - this._tokenreg = null; - this._fetchingBalances = false; - this._fetchingTokens = false; - this._fetchedTokens = false; - this._tokenregSubId = null; this._tokenregMetaSubId = null; // Throttled `retrieveTokens` function - // that gets called max once every 20s - this._throttledRetrieveTokens = throttle( - this._retrieveTokens, - 20 * 1000, + // that gets called max once every 40s + this.longThrottledFetch = throttle( + this.fetchBalances, + 40 * 1000, + { trailing: true } + ); + + this.shortThrottledFetch = throttle( + this.fetchBalances, + 2 * 1000, + { trailing: true } + ); + + // Fetch all tokens every 2 minutes + this.throttledTokensFetch = throttle( + this.fetchTokens, + 60 * 1000, { trailing: true } ); } start () { - this._subscribeBlockNumber(); - this._subscribeAccountsInfo(); - this._retrieveTokens(); + this.subscribeBlockNumber(); + this.subscribeAccountsInfo(); + + this.loadTokens(); } - _subscribeAccountsInfo () { + subscribeAccountsInfo () { this._api .subscribe('parity_accountsInfo', (error, accountsInfo) => { if (error) { return; } - this._accountsInfo = accountsInfo; - this._retrieveTokens(); + this.fetchBalances(); }) .catch((error) => { console.warn('_subscribeAccountsInfo', error); }); } - _subscribeBlockNumber () { + subscribeBlockNumber () { this._api .subscribe('eth_blockNumber', (error) => { if (error) { @@ -86,123 +81,63 @@ export default class Balances { const { syncing } = this._store.getState().nodeStatus; + this.throttledTokensFetch(); + // If syncing, only retrieve balances once every // few seconds if (syncing) { - return this._throttledRetrieveTokens(); + this.shortThrottledFetch(); + return this.longThrottledFetch(); } - this._throttledRetrieveTokens.cancel(); - this._retrieveTokens(); + this.longThrottledFetch.cancel(); + return this.shortThrottledFetch(); }) .catch((error) => { console.warn('_subscribeBlockNumber', error); }); } - getTokenRegistry () { - if (this._tokenreg) { - return Promise.resolve(this._tokenreg); - } - - return Contracts.get().tokenReg - .getContract() - .then((tokenreg) => { - this._tokenreg = tokenreg; - this.attachToTokens(); - - return tokenreg; - }); + fetchBalances () { + this._store.dispatch(fetchBalances()); } - _retrieveTokens () { - if (this._fetchingTokens) { - return; - } + fetchTokens () { + this._store.dispatch(fetchTokensBalances()); + } - if (this._fetchedTokens) { - return this._retrieveBalances(); - } - - this._fetchingTokens = true; - this._fetchedTokens = false; + getTokenRegistry () { + return Contracts.get().tokenReg.getContract(); + } + loadTokens () { this .getTokenRegistry() .then((tokenreg) => { - return tokenreg.instance.tokenCount - .call() - .then((numTokens) => { - const promises = []; + this._store.dispatch(setTokenReg(tokenreg)); + this._store.dispatch(loadTokens()); - for (let i = 0; i < numTokens.toNumber(); i++) { - promises.push(this.fetchTokenInfo(tokenreg, i)); - } - - return Promise.all(promises); - }); - }) - .then(() => { - this._fetchingTokens = false; - this._fetchedTokens = true; - - this._store.dispatch(getTokens(this._tokens)); - this._retrieveBalances(); + return this.attachToTokens(tokenreg); }) .catch((error) => { - console.warn('balances::_retrieveTokens', error); + console.warn('balances::loadTokens', error); }); } - _retrieveBalances () { - if (this._fetchingBalances) { - return; - } - - if (!this._accountsInfo) { - return; - } - - this._fetchingBalances = true; - - const addresses = Object - .keys(this._accountsInfo) - .filter((address) => { - const account = this._accountsInfo[address]; - return !account.meta || !account.meta.deleted; - }); - - this._balances = {}; - - Promise - .all(addresses.map((a) => this.fetchAccountBalance(a))) - .then((balances) => { - addresses.forEach((a, idx) => { - this._balances[a] = balances[idx]; - }); - - this._store.dispatch(getBalances(this._balances)); - this._fetchingBalances = false; - }) - .catch((error) => { - console.warn('_retrieveBalances', error); - this._fetchingBalances = false; - }); + attachToTokens (tokenreg) { + return Promise + .all([ + this.attachToTokenMetaChange(tokenreg), + this.attachToNewToken(tokenreg) + ]); } - attachToTokens () { - this.attachToTokenMetaChange(); - this.attachToNewToken(); - } - - attachToNewToken () { + attachToNewToken (tokenreg) { if (this._tokenregSubId) { - return; + return Promise.resolve(); } - this._tokenreg - .instance - .Registered + return tokenreg.instance.Registered .subscribe({ fromBlock: 0, toBlock: 'latest', @@ -212,138 +147,38 @@ export default class Balances { return console.error('balances::attachToNewToken', 'failed to attach to tokenreg Registered', error.toString(), error.stack); } - const promises = logs.map((log) => { - const id = log.params.id.value.toNumber(); - return this.fetchTokenInfo(this._tokenreg, id); - }); - - return Promise.all(promises); + this.handleTokensLogs(logs); }) .then((tokenregSubId) => { this._tokenregSubId = tokenregSubId; - }) - .catch((e) => { - console.warn('balances::attachToNewToken', e); }); } - attachToTokenMetaChange () { + attachToTokenMetaChange (tokenreg) { if (this._tokenregMetaSubId) { - return; + return Promise.resolve(); } - this._tokenreg - .instance - .MetaChanged + return tokenreg.instance.MetaChanged .subscribe({ fromBlock: 0, toBlock: 'latest', - topics: [ null, this._api.util.asciiToHex('IMG') ], + topics: [ null, padRight(this._api.util.asciiToHex('IMG'), 32) ], skipInitFetch: true }, (error, logs) => { if (error) { return console.error('balances::attachToTokenMetaChange', 'failed to attach to tokenreg MetaChanged', error.toString(), error.stack); } - // In case multiple logs for same token - // in one block. Take the last value. - const tokens = logs - .filter((log) => log.type === 'mined') - .reduce((_tokens, log) => { - const id = log.params.id.value.toNumber(); - const image = log.params.value.value; - - const token = Object.values(this._tokens).find((c) => c.id === id); - const { address } = token; - - _tokens[address] = { address, id, image }; - return _tokens; - }, {}); - - Object - .values(tokens) - .forEach((token) => { - const { address, image } = token; - - if (this._images[address] !== image.toString()) { - this._store.dispatch(setAddressImage(address, image)); - this._images[address] = image.toString(); - } - }); + this.handleTokensLogs(logs); }) .then((tokenregMetaSubId) => { this._tokenregMetaSubId = tokenregMetaSubId; - }) - .catch((e) => { - console.warn('balances::attachToTokenMetaChange', e); }); } - fetchTokenInfo (tokenreg, tokenId) { - return Promise - .all([ - tokenreg.instance.token.call({}, [tokenId]), - tokenreg.instance.meta.call({}, [tokenId, 'IMG']) - ]) - .then(([ tokenData, image ]) => { - const [ address, tag, format, name ] = tokenData; - const contract = this._api.newContract(abis.eip20, address); - - if (this._images[address] !== image.toString()) { - this._store.dispatch(setAddressImage(address, image)); - this._images[address] = image.toString(); - } - - const token = { - format: format.toString(), - id: tokenId, - - address, - tag, - name, - contract - }; - - this._tokens[address] = token; - - return token; - }) - .catch((e) => { - console.warn('balances::fetchTokenInfo', `couldn't fetch token #${tokenId}`, e); - }); - } - - /** - * TODO?: txCount is only shown on an address page, so we - * might not need to fetch it for each address for each block, - * but only for one address when the user is on the account - * view. - */ - fetchAccountBalance (address) { - const _tokens = Object.values(this._tokens); - const tokensPromises = _tokens - .map((token) => { - return token.contract.instance.balanceOf.call({}, [ address ]); - }); - - return Promise - .all([ - this._api.eth.getTransactionCount(address), - this._api.eth.getBalance(address) - ].concat(tokensPromises)) - .then(([ txCount, ethBalance, ...tokensBalance ]) => { - const tokens = [] - .concat( - { token: ETH, value: ethBalance }, - _tokens - .map((token, index) => ({ - token, - value: tokensBalance[index] - })) - ); - - const balance = { txCount, tokens }; - return balance; - }); + handleTokensLogs (logs) { + const tokenIds = logs.map((log) => log.params.id.value.toNumber()); + this._store.dispatch(fetchTokens(tokenIds)); } } diff --git a/js/src/redux/providers/balancesActions.js b/js/src/redux/providers/balancesActions.js index 2771c455e..f5d602b73 100644 --- a/js/src/redux/providers/balancesActions.js +++ b/js/src/redux/providers/balancesActions.js @@ -14,16 +14,354 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export function getBalances (balances) { +import { range, uniq, isEqual } from 'lodash'; + +import { hashToImageUrl } from './imagesReducer'; +import { setAddressImage } from './imagesActions'; + +import * as ABIS from '../../contracts/abi'; +import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png'; + +const ETH = { + name: 'Ethereum', + tag: 'ETH', + image: imagesEthereum +}; + +export function setBalances (balances) { return { - type: 'getBalances', + type: 'setBalances', balances }; } -export function getTokens (tokens) { +export function setTokens (tokens) { return { - type: 'getTokens', + type: 'setTokens', tokens }; } + +export function setTokenReg (tokenreg) { + return { + type: 'setTokenReg', + tokenreg + }; +} + +export function setTokensFilter (tokensFilter) { + return { + type: 'setTokensFilter', + tokensFilter + }; +} + +export function setTokenImage (tokenAddress, image) { + return { + type: 'setTokenImage', + tokenAddress, image + }; +} + +export function loadTokens () { + return (dispatch, getState) => { + const { tokenreg } = getState().balances; + + return tokenreg.instance.tokenCount + .call() + .then((numTokens) => { + const tokenIds = range(numTokens.toNumber()); + dispatch(fetchTokens(tokenIds)); + }) + .catch((error) => { + console.warn('balances::loadTokens', error); + }); + }; +} + +export function fetchTokens (_tokenIds) { + const tokenIds = uniq(_tokenIds || []); + return (dispatch, getState) => { + const { api, images, balances } = getState(); + const { tokenreg } = balances; + + return Promise + .all(tokenIds.map((id) => fetchTokenInfo(tokenreg, id, api))) + .then((tokens) => { + // dispatch only the changed images + tokens + .forEach((token) => { + const { image, address } = token; + + if (images[address] === image) { + return; + } + + dispatch(setTokenImage(address, image)); + dispatch(setAddressImage(address, image, true)); + }); + + dispatch(setTokens(tokens)); + dispatch(fetchBalances()); + }) + .catch((error) => { + console.warn('balances::fetchTokens', error); + }); + }; +} + +export function fetchBalances (_addresses) { + return (dispatch, getState) => { + const { api, personal } = getState(); + const { visibleAccounts } = personal; + + const addresses = uniq(_addresses || visibleAccounts || []); + + if (addresses.length === 0) { + return Promise.resolve(); + } + + const fullFetch = addresses.length === 1; + + return Promise + .all(addresses.map((addr) => fetchAccount(addr, api, fullFetch))) + .then((accountsBalances) => { + const balances = {}; + + addresses.forEach((addr, idx) => { + balances[addr] = accountsBalances[idx]; + }); + + dispatch(setBalances(balances)); + updateTokensFilter(addresses)(dispatch, getState); + }) + .catch((error) => { + console.warn('balances::fetchBalances', error); + }); + }; +} + +export function updateTokensFilter (_addresses, _tokens) { + return (dispatch, getState) => { + const { api, balances, personal } = getState(); + const { visibleAccounts } = personal; + const { tokensFilter } = balances; + + const addresses = uniq(_addresses || visibleAccounts || []).sort(); + const tokens = _tokens || Object.values(balances.tokens) || []; + const tokenAddresses = tokens.map((t) => t.address).sort(); + + if (tokensFilter.filterFromId || tokensFilter.filterToId) { + const sameTokens = isEqual(tokenAddresses, tokensFilter.tokenAddresses); + const sameAddresses = isEqual(addresses, tokensFilter.addresses); + + if (sameTokens && sameAddresses) { + return queryTokensFilter(tokensFilter)(dispatch, getState); + } + } + + let promise = Promise.resolve(); + + if (tokensFilter.filterFromId) { + promise = promise.then(() => api.eth.uninstallFilter(tokensFilter.filterFromId)); + } + + if (tokensFilter.filterToId) { + promise = promise.then(() => api.eth.uninstallFilter(tokensFilter.filterToId)); + } + + if (tokenAddresses.length === 0 || addresses.length === 0) { + return promise; + } + + const TRANSFER_SIGNATURE = api.util.sha3('Transfer(address,address,uint256)'); + const topicsFrom = [ TRANSFER_SIGNATURE, addresses, null ]; + const topicsTo = [ TRANSFER_SIGNATURE, null, addresses ]; + + const options = { + fromBlock: 0, + toBlock: 'pending', + address: tokenAddresses + }; + + const optionsFrom = { + ...options, + topics: topicsFrom + }; + + const optionsTo = { + ...options, + topics: topicsTo + }; + + const newFilters = Promise.all([ + api.eth.newFilter(optionsFrom), + api.eth.newFilter(optionsTo) + ]); + + promise + .then(() => newFilters) + .then(([ filterFromId, filterToId ]) => { + const nextTokensFilter = { + filterFromId, filterToId, + addresses, tokenAddresses + }; + + dispatch(setTokensFilter(nextTokensFilter)); + fetchTokensBalances(addresses, tokens)(dispatch, getState); + }) + .catch((error) => { + console.warn('balances::updateTokensFilter', error); + }); + }; +} + +export function queryTokensFilter (tokensFilter) { + return (dispatch, getState) => { + const { api, personal, balances } = getState(); + const { visibleAccounts } = personal; + const visibleAddresses = visibleAccounts.map((a) => a.toLowerCase()); + + Promise + .all([ + api.eth.getFilterChanges(tokensFilter.filterFromId), + api.eth.getFilterChanges(tokensFilter.filterToId) + ]) + .then(([ logsFrom, logsTo ]) => { + const addresses = []; + const tokenAddresses = []; + + logsFrom + .concat(logsTo) + .forEach((log) => { + const tokenAddress = log.address; + const fromAddress = '0x' + log.topics[1].slice(-40); + const toAddress = '0x' + log.topics[2].slice(-40); + + const fromIdx = visibleAddresses.indexOf(fromAddress); + const toIdx = visibleAddresses.indexOf(toAddress); + + if (fromIdx > -1) { + addresses.push(visibleAccounts[fromIdx]); + } + + if (toIdx > -1) { + addresses.push(visibleAccounts[toIdx]); + } + + tokenAddresses.push(tokenAddress); + }); + + if (addresses.length === 0) { + return; + } + + const tokens = balances.tokens.filter((t) => tokenAddresses.includes(t.address)); + + fetchTokensBalances(uniq(addresses), tokens)(dispatch, getState); + }); + }; +} + +export function fetchTokensBalances (_addresses = null, _tokens = null) { + return (dispatch, getState) => { + const { api, personal, balances } = getState(); + const { visibleAccounts } = personal; + + const addresses = _addresses || visibleAccounts; + const tokens = _tokens || Object.values(balances.tokens); + + if (addresses.length === 0) { + return Promise.resolve(); + } + + return Promise + .all(addresses.map((addr) => fetchTokensBalance(addr, tokens, api))) + .then((tokensBalances) => { + const balances = {}; + + addresses.forEach((addr, idx) => { + balances[addr] = tokensBalances[idx]; + }); + + dispatch(setBalances(balances)); + }) + .catch((error) => { + console.warn('balances::fetchTokensBalances', error); + }); + }; +} + +function fetchAccount (address, api, full = false) { + const promises = [ api.eth.getBalance(address) ]; + + if (full) { + promises.push(api.eth.getTransactionCount(address)); + } + + return Promise + .all(promises) + .then(([ ethBalance, txCount ]) => { + const tokens = [ { token: ETH, value: ethBalance } ]; + const balance = { tokens }; + + if (full) { + balance.txCount = txCount; + } + + return balance; + }) + .catch((error) => { + console.warn('balances::fetchAccountBalance', `couldn't fetch balance for account #${address}`, error); + }); +} + +function fetchTokensBalance (address, _tokens, api) { + const tokensPromises = _tokens + .map((token) => { + return token.contract.instance.balanceOf.call({}, [ address ]); + }); + + return Promise + .all(tokensPromises) + .then((tokensBalance) => { + const tokens = _tokens + .map((token, index) => ({ + token, + value: tokensBalance[index] + })); + + const balance = { tokens }; + return balance; + }) + .catch((error) => { + console.warn('balances::fetchTokensBalance', `couldn't fetch tokens balance for account #${address}`, error); + }); +} + +function fetchTokenInfo (tokenreg, tokenId, api, dispatch) { + return Promise + .all([ + tokenreg.instance.token.call({}, [tokenId]), + tokenreg.instance.meta.call({}, [tokenId, 'IMG']) + ]) + .then(([ tokenData, image ]) => { + const [ address, tag, format, name ] = tokenData; + const contract = api.newContract(ABIS.eip20, address); + + const token = { + format: format.toString(), + id: tokenId, + image: hashToImageUrl(image), + address, + tag, + name, + contract + }; + + return token; + }) + .catch((error) => { + console.warn('balances::fetchTokenInfo', `couldn't fetch token #${tokenId}`, error); + }); +} diff --git a/js/src/redux/providers/balancesReducer.js b/js/src/redux/providers/balancesReducer.js index ea28a5217..f26f08f7d 100644 --- a/js/src/redux/providers/balancesReducer.js +++ b/js/src/redux/providers/balancesReducer.js @@ -15,22 +15,92 @@ // along with Parity. If not, see . import { handleActions } from 'redux-actions'; +import BigNumber from 'bignumber.js'; const initialState = { balances: {}, - tokens: {} + tokens: {}, + tokenreg: null, + tokensFilter: {} }; export default handleActions({ - getBalances (state, action) { - const { balances } = action; + setBalances (state, action) { + const nextBalances = action.balances; + const prevBalances = state.balances; + const balances = { ...prevBalances }; + + Object.keys(nextBalances).forEach((address) => { + if (!balances[address]) { + balances[address] = Object.assign({}, nextBalances[address]); + return; + } + + const balance = Object.assign({}, balances[address]); + const { tokens, txCount = balance.txCount } = nextBalances[address]; + const nextTokens = [].concat(balance.tokens); + + tokens.forEach((t) => { + const { token, value } = t; + const { tag } = token; + + const tokenIndex = nextTokens.findIndex((tok) => tok.token.tag === tag); + + if (tokenIndex === -1) { + nextTokens.push({ + token, + value + }); + } else { + nextTokens[tokenIndex] = { token, value }; + } + }); + + balances[address] = Object.assign({}, { txCount: txCount || new BigNumber(0), tokens: nextTokens }); + }); return Object.assign({}, state, { balances }); }, - getTokens (state, action) { + setTokens (state, action) { const { tokens } = action; - return Object.assign({}, state, { tokens }); + }, + + setTokenImage (state, action) { + const { tokenAddress, image } = action; + const { balances } = state; + const nextBalances = {}; + + Object.keys(balances).forEach((address) => { + const tokenIndex = balances[address].tokens.findIndex((t) => t.token.address === tokenAddress); + + if (tokenIndex === -1 || balances[address].tokens[tokenIndex].value.equals(0)) { + return; + } + + const tokens = [].concat(balances[address].tokens); + tokens[tokenIndex].token = { + ...tokens[tokenIndex].token, + image + }; + + nextBalances[address] = { + ...balances[address], + tokens + }; + }); + + return Object.assign({}, state, { balance: { ...balances, nextBalances } }); + }, + + setTokenReg (state, action) { + const { tokenreg } = action; + return Object.assign({}, state, { tokenreg }); + }, + + setTokensFilter (state, action) { + const { tokensFilter } = action; + return Object.assign({}, state, { tokensFilter }); } }, initialState); diff --git a/js/src/redux/providers/imagesActions.js b/js/src/redux/providers/imagesActions.js index ce9221a3b..8ef3c3b39 100644 --- a/js/src/redux/providers/imagesActions.js +++ b/js/src/redux/providers/imagesActions.js @@ -14,10 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export function setAddressImage (address, hashArray) { +export function setAddressImage (address, hashArray, converted = false) { return { type: 'setAddressImage', address, - hashArray + hashArray, + converted }; } diff --git a/js/src/redux/providers/imagesReducer.js b/js/src/redux/providers/imagesReducer.js index 396576cc8..3f91f262f 100644 --- a/js/src/redux/providers/imagesReducer.js +++ b/js/src/redux/providers/imagesReducer.js @@ -31,10 +31,12 @@ export function hashToImageUrl (hashArray) { export default handleActions({ setAddressImage (state, action) { - const { address, hashArray } = action; + const { address, hashArray, converted } = action; + + const image = converted ? hashArray : hashToImageUrl(hashArray); return Object.assign({}, state, { - [address]: hashToImageUrl(hashArray) + [address]: image }); } }, initialState); diff --git a/js/src/redux/providers/personalActions.js b/js/src/redux/providers/personalActions.js index 2e422cb1a..d8f925b2d 100644 --- a/js/src/redux/providers/personalActions.js +++ b/js/src/redux/providers/personalActions.js @@ -14,9 +14,33 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import { isEqual } from 'lodash'; + +import { fetchBalances } from './balancesActions'; + export function personalAccountsInfo (accountsInfo) { return { type: 'personalAccountsInfo', accountsInfo }; } + +export function _setVisibleAccounts (addresses) { + return { + type: 'setVisibleAccounts', + addresses + }; +} + +export function setVisibleAccounts (addresses) { + return (dispatch, getState) => { + const { visibleAccounts } = getState().personal; + + if (isEqual(addresses.sort(), visibleAccounts.sort())) { + return; + } + + dispatch(fetchBalances(addresses)); + dispatch(_setVisibleAccounts(addresses)); + }; +} diff --git a/js/src/redux/providers/personalReducer.js b/js/src/redux/providers/personalReducer.js index 6d35610e9..622c81ee5 100644 --- a/js/src/redux/providers/personalReducer.js +++ b/js/src/redux/providers/personalReducer.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import { handleActions } from 'redux-actions'; +import { isEqual } from 'lodash'; const initialState = { accountsInfo: {}, @@ -23,7 +24,8 @@ const initialState = { contacts: {}, hasContacts: false, contracts: {}, - hasContracts: false + hasContracts: false, + visibleAccounts: [] }; export default handleActions({ @@ -55,5 +57,17 @@ export default handleActions({ contracts, hasContracts: Object.keys(contracts).length !== 0 }); + }, + + setVisibleAccounts (state, action) { + const addresses = (action.addresses || []).sort(); + + if (isEqual(addresses, state.addresses)) { + return state; + } + + return Object.assign({}, state, { + visibleAccounts: addresses + }); } }, initialState); diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index cedf62d89..936fe3f25 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -30,6 +30,8 @@ export default class Status { this._pollPingTimeoutId = null; this._longStatusTimeoutId = null; + + this._timestamp = Date.now(); } start () { @@ -131,10 +133,10 @@ export default class Status { secureToken }; - const gotReconnected = !this._apiStatus.isConnected && apiStatus.isConnected; + const gotConnected = !this._apiStatus.isConnected && apiStatus.isConnected; - if (gotReconnected) { - this._pollLongStatus(true); + if (gotConnected) { + this._pollLongStatus(); this._store.dispatch(statusCollection({ isPingable: true })); } @@ -156,20 +158,22 @@ export default class Status { const { refreshStatus } = this._store.getState().nodeStatus; - const statusPromises = [ this._api.eth.syncing(), this._api.parity.netPeers() ]; + const statusPromises = [ this._api.eth.syncing() ]; if (refreshStatus) { + statusPromises.push(this._api.parity.netPeers()); statusPromises.push(this._api.eth.hashrate()); } Promise .all(statusPromises) - .then(([ syncing, netPeers, ...statusResults ]) => { + .then(([ syncing, ...statusResults ]) => { const status = statusResults.length === 0 - ? { syncing, netPeers } + ? { syncing } : { - syncing, netPeers, - hashrate: statusResults[0] + syncing, + netPeers: statusResults[0], + hashrate: statusResults[1] }; if (!isEqual(status, this._status)) { @@ -223,7 +227,7 @@ export default class Status { * fetched every 30s just in case, and whenever * the client got reconnected. */ - _pollLongStatus = (newConnection = false) => { + _pollLongStatus = () => { if (!this._api.isConnected) { return; } @@ -241,34 +245,33 @@ export default class Status { Promise .all([ + this._api.parity.netPeers(), this._api.web3.clientVersion(), this._api.net.version(), this._api.parity.defaultExtraData(), this._api.parity.netChain(), this._api.parity.netPort(), this._api.parity.rpcSettings(), - newConnection ? Promise.resolve(null) : this._api.parity.enode() + this._api.parity.enode() ]) .then(([ - clientVersion, netVersion, defaultExtraData, netChain, netPort, rpcSettings, enode + netPeers, clientVersion, netVersion, defaultExtraData, netChain, netPort, rpcSettings, enode ]) => { const isTest = netVersion === '2' || // morden netVersion === '3'; // ropsten const longStatus = { + netPeers, clientVersion, defaultExtraData, netChain, netPort, rpcSettings, - isTest + isTest, + enode }; - if (enode) { - longStatus.enode = enode; - } - if (!isEqual(longStatus, this._longStatus)) { this._store.dispatch(statusCollection(longStatus)); this._longStatus = longStatus; @@ -278,7 +281,7 @@ export default class Status { console.error('_pollLongStatus', error); }); - nextTimeout(newConnection ? 5000 : 30000); + nextTimeout(60000); } _pollLogs = () => { diff --git a/js/src/redux/providers/statusReducer.js b/js/src/redux/providers/statusReducer.js index f0ef0cb1b..07ba4af5b 100644 --- a/js/src/redux/providers/statusReducer.js +++ b/js/src/redux/providers/statusReducer.js @@ -43,7 +43,7 @@ const initialState = { isConnected: false, isConnecting: false, isPingable: false, - isTest: false, + isTest: undefined, refreshStatus: false, traceMode: undefined }; diff --git a/js/src/secureApi.js b/js/src/secureApi.js index af62da2cf..8243420b5 100644 --- a/js/src/secureApi.js +++ b/js/src/secureApi.js @@ -77,6 +77,12 @@ export default class SecureApi extends Api { return this ._checkNodeUp() .then((isNodeUp) => { + const { timestamp } = lastError; + + if ((Date.now() - timestamp) > 250) { + return nextTick(); + } + const nextToken = this._tokensToTry[0] || 'initial'; const nextState = nextToken !== 'initial' ? 0 : 1; @@ -89,7 +95,7 @@ export default class SecureApi extends Api { this.updateToken(nextToken, nextState); } - nextTick(); + return nextTick(); }); } break; diff --git a/js/src/ui/BlockStatus/blockStatus.js b/js/src/ui/BlockStatus/blockStatus.js index 98e10f504..8abd5d656 100644 --- a/js/src/ui/BlockStatus/blockStatus.js +++ b/js/src/ui/BlockStatus/blockStatus.js @@ -44,7 +44,7 @@ class BlockStatus extends Component { ); } - if (!syncing.warpChunksAmount.eq(syncing.warpChunksProcessed)) { + if (syncing.warpChunksAmount && syncing.warpChunksProcessed && !syncing.warpChunksAmount.eq(syncing.warpChunksProcessed)) { return (
{ syncing.warpChunksProcessed.mul(100).div(syncing.warpChunksAmount).toFormat(2) }% warp restore diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js index 66f0a36b9..b28abeb69 100644 --- a/js/src/views/Account/Header/header.js +++ b/js/src/views/Account/Header/header.js @@ -28,20 +28,7 @@ export default class Header extends Component { static propTypes = { account: PropTypes.object, - balance: PropTypes.object, - isTest: PropTypes.bool - } - - state = { - name: null - } - - componentWillMount () { - this.setName(); - } - - componentWillReceiveProps () { - this.setName(); + balance: PropTypes.object } render () { @@ -87,13 +74,13 @@ export default class Header extends Component { } renderTxCount () { - const { isTest, balance } = this.props; + const { balance } = this.props; if (!balance) { return null; } - const txCount = balance.txCount.sub(isTest ? 0x100000 : 0); + const { txCount } = balance; return (
@@ -101,28 +88,4 @@ export default class Header extends Component {
); } - - onSubmitName = (name) => { - const { api } = this.context; - const { account } = this.props; - - this.setState({ name }, () => { - api.parity - .setAccountName(account.address, name) - .catch((error) => { - console.error(error); - }); - }); - } - - setName () { - const { account } = this.props; - - if (account && account.name !== this.propName) { - this.propName = account.name; - this.setState({ - name: account.name - }); - } - } } diff --git a/js/src/views/Account/Transactions/transactions.js b/js/src/views/Account/Transactions/transactions.js index 3e14dd923..2261284a1 100644 --- a/js/src/views/Account/Transactions/transactions.js +++ b/js/src/views/Account/Transactions/transactions.js @@ -143,6 +143,12 @@ class Transactions extends Component { getTransactions = (props) => { const { isTest, address, traceMode } = props; + // Don't fetch the transactions if we don't know in which + // network we are yet... + if (isTest === undefined) { + return; + } + return this .fetchTransactions(isTest, address, traceMode) .then(transactions => { diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js index b36f58618..1181b7f73 100644 --- a/js/src/views/Account/account.js +++ b/js/src/views/Account/account.js @@ -30,6 +30,7 @@ import shapeshiftBtn from '../../../assets/images/shapeshift-btn.png'; import Header from './Header'; import Transactions from './Transactions'; +import { setVisibleAccounts } from '../../redux/providers/personalActions'; import VerificationStore from '../../modals/SMSVerification/store'; @@ -41,11 +42,12 @@ class Account extends Component { } static propTypes = { + setVisibleAccounts: PropTypes.func.isRequired, + images: PropTypes.object.isRequired, + params: PropTypes.object, accounts: PropTypes.object, - balances: PropTypes.object, - images: PropTypes.object.isRequired, - isTest: PropTypes.bool + balances: PropTypes.object } propName = null @@ -66,10 +68,30 @@ class Account extends Component { const verificationStore = new VerificationStore(api, address); this.setState({ verificationStore }); + this.setVisibleAccounts(); + } + + componentWillReceiveProps (nextProps) { + const prevAddress = this.props.params.address; + const nextAddress = nextProps.params.address; + + if (prevAddress !== nextAddress) { + this.setVisibleAccounts(nextProps); + } + } + + componentWillUnmount () { + this.props.setVisibleAccounts([]); + } + + setVisibleAccounts (props = this.props) { + const { params, setVisibleAccounts } = props; + const addresses = [ params.address ]; + setVisibleAccounts(addresses); } render () { - const { accounts, balances, isTest } = this.props; + const { accounts, balances } = this.props; const { address } = this.props.params; const account = (accounts || {})[address]; @@ -90,7 +112,6 @@ class Account extends Component { { this.renderActionbar() }
token.token.tag.toLowerCase() === 'eth') - .value; - const ethB = balanceB.tokens - .find(token => token.token.tag.toLowerCase() === 'eth') - .value; + const ethA = balanceA.tokens.find(token => token.token.tag.toLowerCase() === 'eth'); + const ethB = balanceB.tokens.find(token => token.token.tag.toLowerCase() === 'eth'); - return -1 * ethA.comparedTo(ethB); + if (!ethA && !ethB) return 0; + if (ethA && !ethB) return -1; + if (!ethA && ethB) return 1; + + return -1 * ethA.value.comparedTo(ethB.value); } if (key === 'tags') { diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js index 88249bb1c..4baf838ce 100644 --- a/js/src/views/Accounts/Summary/summary.js +++ b/js/src/views/Accounts/Summary/summary.js @@ -38,10 +38,6 @@ export default class Summary extends Component { noLink: false }; - state = { - name: 'Unnamed' - }; - shouldComponentUpdate (nextProps) { const prev = { link: this.props.link, name: this.props.name, @@ -66,8 +62,8 @@ export default class Summary extends Component { return true; } - const prevValues = prevTokens.map((t) => t.value.toNumber()); - const nextValues = nextTokens.map((t) => t.value.toNumber()); + const prevValues = prevTokens.map((t) => ({ value: t.value.toNumber(), image: t.token.image })); + const nextValues = nextTokens.map((t) => ({ value: t.value.toNumber(), image: t.token.image })); if (!isEqual(prevValues, nextValues)) { return true; diff --git a/js/src/views/Accounts/accounts.js b/js/src/views/Accounts/accounts.js index 0075a15a2..df55a47e7 100644 --- a/js/src/views/Accounts/accounts.js +++ b/js/src/views/Accounts/accounts.js @@ -18,11 +18,12 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import ContentAdd from 'material-ui/svg-icons/content/add'; -import { uniq } from 'lodash'; +import { uniq, isEqual } from 'lodash'; import List from './List'; import { CreateAccount } from '../../modals'; import { Actionbar, ActionbarExport, ActionbarSearch, ActionbarSort, Button, Page, Tooltip } from '../../ui'; +import { setVisibleAccounts } from '../../redux/providers/personalActions'; import styles from './accounts.css'; @@ -32,6 +33,8 @@ class Accounts extends Component { } static propTypes = { + setVisibleAccounts: PropTypes.func.isRequired, + accounts: PropTypes.object, hasAccounts: PropTypes.bool, balances: PropTypes.object @@ -50,6 +53,27 @@ class Accounts extends Component { window.setTimeout(() => { this.setState({ show: true }); }, 100); + + this.setVisibleAccounts(); + } + + componentWillReceiveProps (nextProps) { + const prevAddresses = Object.keys(this.props.accounts); + const nextAddresses = Object.keys(nextProps.accounts); + + if (prevAddresses.length !== nextAddresses.length || !isEqual(prevAddresses.sort(), nextAddresses.sort())) { + this.setVisibleAccounts(nextProps); + } + } + + componentWillUnmount () { + this.props.setVisibleAccounts([]); + } + + setVisibleAccounts (props = this.props) { + const { accounts, setVisibleAccounts } = props; + const addresses = Object.keys(accounts); + setVisibleAccounts(addresses); } render () { @@ -206,7 +230,9 @@ function mapStateToProps (state) { } function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); + return bindActionCreators({ + setVisibleAccounts + }, dispatch); } export default connect( diff --git a/js/src/views/Address/address.js b/js/src/views/Address/address.js index 210f63b99..f0a452185 100644 --- a/js/src/views/Address/address.js +++ b/js/src/views/Address/address.js @@ -26,6 +26,7 @@ import { Actionbar, Button, Page } from '../../ui'; import Header from '../Account/Header'; import Transactions from '../Account/Transactions'; import Delete from './Delete'; +import { setVisibleAccounts } from '../../redux/providers/personalActions'; import styles from './address.css'; @@ -36,9 +37,10 @@ class Address extends Component { } static propTypes = { + setVisibleAccounts: PropTypes.func.isRequired, + contacts: PropTypes.object, balances: PropTypes.object, - isTest: PropTypes.bool, params: PropTypes.object } @@ -47,8 +49,31 @@ class Address extends Component { showEditDialog: false } + componentDidMount () { + this.setVisibleAccounts(); + } + + componentWillReceiveProps (nextProps) { + const prevAddress = this.props.params.address; + const nextAddress = nextProps.params.address; + + if (prevAddress !== nextAddress) { + this.setVisibleAccounts(nextProps); + } + } + + componentWillUnmount () { + this.props.setVisibleAccounts([]); + } + + setVisibleAccounts (props = this.props) { + const { params, setVisibleAccounts } = props; + const addresses = [ params.address ]; + setVisibleAccounts(addresses); + } + render () { - const { contacts, balances, isTest } = this.props; + const { contacts, balances } = this.props; const { address } = this.props.params; const { showDeleteDialog } = this.state; @@ -70,7 +95,6 @@ class Address extends Component { onClose={ this.closeDeleteDialog } />
this.setState({ blockSubscriptionId })); } - componentWillReceiveProps (newProps) { - const { accounts, contracts } = newProps; + componentWillReceiveProps (nextProps) { + const { accounts, contracts } = nextProps; if (Object.keys(contracts).length !== Object.keys(this.props.contracts).length) { - this.attachContract(newProps); + this.attachContract(nextProps); } if (Object.keys(accounts).length !== Object.keys(this.props.accounts).length) { - this.setBaseAccount(newProps); + this.setBaseAccount(nextProps); + } + + const prevAddress = this.props.params.address; + const nextAddress = nextProps.params.address; + + if (prevAddress !== nextAddress) { + this.setVisibleAccounts(nextProps); } } @@ -92,6 +104,13 @@ class Contract extends Component { api.unsubscribe(blockSubscriptionId); contract.unsubscribe(subscriptionId); + this.props.setVisibleAccounts([]); + } + + setVisibleAccounts (props = this.props) { + const { params, setVisibleAccounts } = props; + const addresses = [ params.address ]; + setVisibleAccounts(addresses); } render () { @@ -112,7 +131,6 @@ class Contract extends Component { { this.renderExecuteDialog() }
{ return response.ok ? response.json() : null; }) + .then((manifest) => { + if (manifest) { + this._manifests[manifestHash] = manifest; + } + + return manifest; + }) .catch((error) => { console.warn('DappsStore:fetchManifest', error); return null; From 6d6e942d9d291161bfb69f512f2309447585f718 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 25 Nov 2016 16:46:57 +0100 Subject: [PATCH 12/17] Add dappreg link to apps list (#3568) * Fix rendering when contract is empty * Add registration dapp to app list --- js/src/dapps/dappreg/ButtonBar/buttonBar.js | 4 ++-- js/src/dapps/dappreg/Dapp/dapp.js | 4 ++++ js/src/dapps/dappreg/SelectDapp/selectDapp.js | 4 ++++ js/src/dapps/dappreg/dappsStore.js | 7 +++++-- js/src/views/Dapps/builtin.json | 10 ++++++++++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/js/src/dapps/dappreg/ButtonBar/buttonBar.js b/js/src/dapps/dappreg/ButtonBar/buttonBar.js index 289def0ea..074c527d0 100644 --- a/js/src/dapps/dappreg/ButtonBar/buttonBar.js +++ b/js/src/dapps/dappreg/ButtonBar/buttonBar.js @@ -50,12 +50,12 @@ export default class ButtonBar extends Component { key='delete' label='Delete' warning - disabled={ !this.dappsStore.currentApp.isOwner && !this.dappsStore.isContractOwner } + disabled={ !this.dappsStore.currentApp || (!this.dappsStore.currentApp.isOwner && !this.dappsStore.isContractOwner) } onClick={ this.onDeleteClick } />,
); } renderContract () { - const { from, fromBalance, to, isTest } = this.props; - - if (to) { - return; - } + const { children, from, fromBalance, isTest } = this.props; return (
@@ -134,6 +122,7 @@ export default class TransactionMainDetails extends Component { Contract
+ { children } ); } diff --git a/js/src/views/Signer/components/TransactionPending/TransactionPending.css b/js/src/views/Signer/components/TransactionPending/TransactionPending.css index 9574826a3..5cd2d10f5 100644 --- a/js/src/views/Signer/components/TransactionPending/TransactionPending.css +++ b/js/src/views/Signer/components/TransactionPending/TransactionPending.css @@ -14,33 +14,14 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see . */ + +@import '../../_layout.css'; + .container { - padding: 25px 0 15px; -} + display: flex; + padding: 1.5em 0 1em; -.transactionDetails { - padding-right: 321px; - width: 100%; - box-sizing: border-box; -} - -.mainContainer { - position: relative; -} - -.mainContainer:after { - clear: both; -} - -.mainContainer > * { - vertical-align: middle; - min-height: 190px; -} - -.inputs { - margin-right: 30px; - margin-left: 30px; - width: 180px; - position: relative; - top: -15px; /* due to material ui weird styling */ + & > * { + vertical-align: middle; + } } diff --git a/js/src/views/Signer/components/TransactionPending/TransactionPending.js b/js/src/views/Signer/components/TransactionPending/TransactionPending.js index 0742c2c76..013d887a5 100644 --- a/js/src/views/Signer/components/TransactionPending/TransactionPending.js +++ b/js/src/views/Signer/components/TransactionPending/TransactionPending.js @@ -70,29 +70,27 @@ export default class TransactionPending extends Component { return (
-
- - - - + -
+ +
); } diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingForm.css b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingForm.css index 37c1a8317..aaea1de8d 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingForm.css +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingForm.css @@ -14,14 +14,13 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see . */ + +@import '../../_layout.css'; + .container { - width: 220px; - padding: 20px 40px 0 40px; - /*border-left: 1px solid #aaa;*/ - position: absolute; - top: 0; - right: 0; - box-sizing: content-box; + box-sizing: border-box; + padding: 1em 1em 0 1em; + flex: 0 0 $statusWidth; } .rejectToggle { diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.css b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.css index d10e634ae..7ab5a46a6 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.css +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.css @@ -15,7 +15,7 @@ /* along with Parity. If not, see . */ .confirmForm { - margin-top: -45px; + margin-top: -2em; } .confirmButton { diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/TransactionPendingFormReject.css b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/TransactionPendingFormReject.css index 39c7f377a..9e91456ca 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/TransactionPendingFormReject.css +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/TransactionPendingFormReject.css @@ -14,6 +14,7 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see . */ + /* the rejection button itself, once .reject has been pressed */ .rejectButton { display: block !important; diff --git a/js/src/views/Signer/components/TransactionSecondaryDetails/TransactionSecondaryDetails.css b/js/src/views/Signer/components/TransactionSecondaryDetails/TransactionSecondaryDetails.css index 3504c7028..ae71b1004 100644 --- a/js/src/views/Signer/components/TransactionSecondaryDetails/TransactionSecondaryDetails.css +++ b/js/src/views/Signer/components/TransactionSecondaryDetails/TransactionSecondaryDetails.css @@ -1,3 +1,24 @@ +/* Copyright 2015, 2016 Ethcore (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +.container { + display: block; +} + .iconsContainer { display: block; text-align: center; @@ -67,4 +88,3 @@ .expandedContainer:empty { padding: 0; } - diff --git a/js/src/views/Signer/components/TransactionSecondaryDetails/TransactionSecondaryDetails.js b/js/src/views/Signer/components/TransactionSecondaryDetails/TransactionSecondaryDetails.js index fb0a329e0..283712ed0 100644 --- a/js/src/views/Signer/components/TransactionSecondaryDetails/TransactionSecondaryDetails.js +++ b/js/src/views/Signer/components/TransactionSecondaryDetails/TransactionSecondaryDetails.js @@ -27,7 +27,6 @@ import styles from './TransactionSecondaryDetails.css'; import * as tUtil from '../util/transaction'; export default class TransactionSecondaryDetails extends Component { - static propTypes = { id: PropTypes.object.isRequired, date: PropTypes.instanceOf(Date), @@ -45,7 +44,7 @@ export default class TransactionSecondaryDetails extends Component { const className = this.props.className || ''; return ( -
+
{ this.renderGasPrice() } { this.renderData() } diff --git a/js/src/views/Signer/containers/Embedded/embedded.css b/js/src/views/Signer/containers/Embedded/embedded.css index cbe12d6dc..94e0f3933 100644 --- a/js/src/views/Signer/containers/Embedded/embedded.css +++ b/js/src/views/Signer/containers/Embedded/embedded.css @@ -14,8 +14,13 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see . */ + +@import '../../_layout.css'; + .signer { - width: 916px; + box-sizing: border-box; + padding: 0; + width: $embedWidth; } .pending { diff --git a/js/src/views/Signer/containers/RequestsPage/RequestsPage.css b/js/src/views/Signer/containers/RequestsPage/RequestsPage.css index 3dbf1cf48..3701c3097 100644 --- a/js/src/views/Signer/containers/RequestsPage/RequestsPage.css +++ b/js/src/views/Signer/containers/RequestsPage/RequestsPage.css @@ -14,6 +14,7 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see . */ + .request { } From 50585763aa87d5f50c520902b1b03f41d64d8325 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 25 Nov 2016 18:43:28 +0100 Subject: [PATCH 15/17] Manually add \r to Windows phrases pre 1.4.5 (#3615) * Manually add \r to Windows phrases pre 1.4.4 * < 1.4.5 * Only support 1.4.x dictionary --- .../RecoveryPhrase/recoveryPhrase.js | 29 +++++++++++++++---- js/src/modals/CreateAccount/createAccount.css | 5 ++++ js/src/modals/CreateAccount/createAccount.js | 16 ++++++++-- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js b/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js index ffa90ef27..b766e247f 100644 --- a/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js +++ b/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { Checkbox } from 'material-ui'; import { Form, Input } from '../../../ui'; @@ -37,6 +38,7 @@ export default class RecoveryPhrase extends Component { password1Error: ERRORS.invalidPassword, password2: '', password2Error: ERRORS.noMatchPassword, + windowsPhrase: false, isValidPass: false, isValidName: false, isValidPhrase: false @@ -47,7 +49,7 @@ export default class RecoveryPhrase extends Component { } render () { - const { accountName, accountNameError, passwordHint, password1, password1Error, password2, password2Error, recoveryPhrase } = this.state; + const { accountName, accountNameError, passwordHint, password1, password1Error, password2, password2Error, recoveryPhrase, windowsPhrase } = this.state; return (
@@ -86,20 +88,26 @@ export default class RecoveryPhrase extends Component { value={ password2 } onChange={ this.onEditPassword2 } />
+
); } updateParent = () => { - const { isValidName, isValidPass, isValidPhrase, accountName, passwordHint, password1, recoveryPhrase } = this.state; + const { isValidName, isValidPass, isValidPhrase, accountName, passwordHint, password1, recoveryPhrase, windowsPhrase } = this.state; const isValid = isValidName && isValidPass && isValidPhrase; this.props.onChange(isValid, { name: accountName, passwordHint, password: password1, - phrase: recoveryPhrase + phrase: recoveryPhrase, + windowsPhrase }); } @@ -109,6 +117,12 @@ export default class RecoveryPhrase extends Component { }); } + onToggleWindowsPhrase = (event) => { + this.setState({ + windowsPhrase: !this.state.windowsPhrase + }, this.updateParent); + } + onEditPhrase = (event) => { const recoveryPhrase = event.target.value .toLowerCase() // wordlists are lowercase @@ -116,15 +130,18 @@ export default class RecoveryPhrase extends Component { .replace(/\s/g, ' ') // replace any whitespace with single space .replace(/ +/g, ' '); // replace multiple spaces with a single space - const parts = recoveryPhrase.split(' '); + const phraseParts = recoveryPhrase + .split(' ') + .map((part) => part.trim()) + .filter((part) => part.length); let recoveryPhraseError = null; - if (!recoveryPhrase || recoveryPhrase.length < 25 || parts.length < 8) { + if (!recoveryPhrase || recoveryPhrase.length < 25 || phraseParts.length < 8) { recoveryPhraseError = ERRORS.noPhrase; } this.setState({ - recoveryPhrase, + recoveryPhrase: phraseParts.join(' '), recoveryPhraseError, isValidPhrase: !recoveryPhraseError }, this.updateParent); diff --git a/js/src/modals/CreateAccount/createAccount.css b/js/src/modals/CreateAccount/createAccount.css index 169cb618a..b87029a36 100644 --- a/js/src/modals/CreateAccount/createAccount.css +++ b/js/src/modals/CreateAccount/createAccount.css @@ -14,6 +14,7 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see . */ + .spaced { line-height: 1.618em; } @@ -67,3 +68,7 @@ .upload>div { margin-right: 0.5em; } + +.checkbox { + margin-top: 2em; +} diff --git a/js/src/modals/CreateAccount/createAccount.js b/js/src/modals/CreateAccount/createAccount.js index 283e91531..e95dab2a7 100644 --- a/js/src/modals/CreateAccount/createAccount.js +++ b/js/src/modals/CreateAccount/createAccount.js @@ -59,6 +59,7 @@ export default class CreateAccount extends Component { passwordHint: null, password: null, phrase: null, + windowsPhrase: false, rawKey: null, json: null, canCreate: false, @@ -200,7 +201,7 @@ export default class CreateAccount extends Component { } onCreate = () => { - const { createType } = this.state; + const { createType, windowsPhrase } = this.state; const { api } = this.context; this.setState({ @@ -208,8 +209,16 @@ export default class CreateAccount extends Component { }); if (createType === 'fromNew' || createType === 'fromPhrase') { + let phrase = this.state.phrase; + if (createType === 'fromPhrase' && windowsPhrase) { + phrase = phrase + .split(' ') // get the words + .map((word) => word === 'misjudged' ? word : `${word}\r`) // add \r after each (except last in dict) + .join(' '); // re-create string + } + return api.parity - .newAccountFromPhrase(this.state.phrase, this.state.password) + .newAccountFromPhrase(phrase, this.state.password) .then((address) => { this.setState({ address }); return api.parity @@ -326,7 +335,7 @@ export default class CreateAccount extends Component { }); } - onChangeDetails = (canCreate, { name, passwordHint, address, password, phrase, rawKey }) => { + onChangeDetails = (canCreate, { name, passwordHint, address, password, phrase, rawKey, windowsPhrase }) => { this.setState({ canCreate, name, @@ -334,6 +343,7 @@ export default class CreateAccount extends Component { address, password, phrase, + windowsPhrase: windowsPhrase || false, rawKey }); } From 8ca1781ab98d1e825fb78c9351fbe843fcaaf76e Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" Date: Sat, 26 Nov 2016 01:57:58 +0700 Subject: [PATCH 16/17] Update gitlab-ci add js-test as diff --- .gitlab-ci.yml | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 366b89d20..bdb2ea5ce 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -468,6 +468,21 @@ test-rust-stable: tags: - rust - rust-stable +js-test: + stage: test + image: ethcore/rust:stable + before_script: + - git submodule update --init --recursive + - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep \.js | wc -l) + - echo $JS_FILES_MODIFIED + - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"; else ./js/scripts/install-deps.sh;fi + script: + - export RUST_BACKTRACE=1 + - echo $JS_FILES_MODIFIED + - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"; else echo "skip rust test"&./js/scripts/lint.sh&./js/scripts/test.sh&./js/scripts/build.sh; fi + tags: + - rust + - rust-stable test-rust-beta: stage: test only: @@ -475,13 +490,10 @@ test-rust-beta: image: ethcore/rust:beta before_script: - git submodule update --init --recursive - - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep \.js | wc -l) - - echo $JS_FILES_MODIFIED - - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"; else ./js/scripts/install-deps.sh;fi script: - export RUST_BACKTRACE=1 - echo $JS_FILES_MODIFIED - - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"&./test.sh $CARGOFLAGS --no-release; else echo "skip rust test"&./js/scripts/lint.sh&./js/scripts/test.sh&./js/scripts/build.sh; fi + - ./test.sh $CARGOFLAGS --no-release tags: - rust - rust-beta @@ -493,13 +505,9 @@ test-rust-nightly: image: ethcore/rust:nightly before_script: - git submodule update --init --recursive - - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep \.js | wc -l) - - echo $JS_FILES_MODIFIED - - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"; else ./js/scripts/install-deps.sh;fi script: - export RUST_BACKTRACE=1 - - echo $JS_FILES_MODIFIED - - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"&./test.sh $CARGOFLAGS --no-release; else echo "skip rust test"&./js/scripts/lint.sh&./js/scripts/test.sh&./js/scripts/build.sh; fi + - ./test.sh $CARGOFLAGS --no-release tags: - rust - rust-nightly From a7037f8e5b2b8b7435189615d8e8aa62ac03e49a Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" Date: Sat, 26 Nov 2016 03:52:29 +0700 Subject: [PATCH 17/17] Update gitlab-ci js-release &->&& --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bdb2ea5ce..4a7ba9d44 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -525,6 +525,6 @@ js-release: - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js build"; else ./js/scripts/install-deps.sh;fi script: - echo $JS_FILES_MODIFIED - - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js build"; else ./js/scripts/build.sh&./js/scripts/release.sh; fi + - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js build"; else ./js/scripts/build.sh&&./js/scripts/release.sh; fi tags: - javascript