diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6df465035..84d10e1bc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -422,10 +422,10 @@ test-rust-stable: image: ethcore/rust:stable before_script: - git submodule update --init --recursive - - export RUST_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep -v ^js/ | wc -l) + - export RUST_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep -v -e ^js -e ^\\. -e ^LICENSE -e ^README.md -e ^appveyor.yml -e ^test.sh -e ^windows/ -e ^scripts/ -e^mac/ -e ^nsis/ | wc -l) script: - export RUST_BACKTRACE=1 - - if [ "$RUST_FILES_MODIFIED" = 0 ]; then echo "Skipping Rust tests since no Rust files modified."; else ./test.sh $CARGOFLAGS; fi + - if [ $RUST_FILES_MODIFIED -eq 0 ]; then echo "Skipping Rust tests since no Rust files modified."; else ./test.sh $CARGOFLAGS; fi tags: - rust - rust-stable @@ -435,9 +435,9 @@ js-test: 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) - - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi + - if [ $JS_FILES_MODIFIED -eq 0 ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi script: - - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS lint since no JS files modified."; else ./js/scripts/lint.sh && ./js/scripts/test.sh && ./js/scripts/build.sh; fi + - if [ $JS_FILES_MODIFIED -eq 0 ]; then echo "Skipping JS lint since no JS files modified."; else ./js/scripts/lint.sh && ./js/scripts/test.sh && ./js/scripts/build.sh; fi tags: - rust - rust-stable @@ -480,9 +480,9 @@ js-release: before_script: - 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 "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi + - if [ $JS_FILES_MODIFIED -eq 0 ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi script: - echo $JS_FILES_MODIFIED - - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS rebuild since no JS files modified."; else ./js/scripts/build.sh && ./js/scripts/release.sh; fi + - if [ $JS_FILES_MODIFIED -eq 0 ]; then echo "Skipping JS rebuild since no JS files modified."; else ./js/scripts/build.sh && ./js/scripts/release.sh; fi tags: - javascript diff --git a/Cargo.lock b/Cargo.lock index 7e970051c..7d485651c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1290,7 +1290,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#3d3b2f9e8e8b0fd62c172240bfd001a317cf2979" +source = "git+https://github.com/ethcore/js-precompiled.git#f982c84ac216cc4f99d056c912e205bcf9341602" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/ethcore/src/miner/miner.rs b/ethcore/src/miner/miner.rs index ad541391d..83a96837f 100644 --- a/ethcore/src/miner/miner.rs +++ b/ethcore/src/miner/miner.rs @@ -1092,20 +1092,6 @@ impl MinerService for Miner { fn chain_new_blocks(&self, chain: &MiningBlockChainClient, _imported: &[H256], _invalid: &[H256], enacted: &[H256], retracted: &[H256]) { trace!(target: "miner", "chain_new_blocks"); - fn fetch_transactions(chain: &MiningBlockChainClient, hash: &H256) -> Vec { - let block = chain - .block(BlockId::Hash(*hash)) - // Client should send message after commit to db and inserting to chain. - .expect("Expected in-chain blocks."); - let block = BlockView::new(&block); - let txs = block.transactions(); - // populate sender - for tx in &txs { - let _sender = tx.sender(); - } - txs - } - // 1. We ignore blocks that were `imported` (because it means that they are not in canon-chain, and transactions // should be still available in the queue. // 2. We ignore blocks that are `invalid` because it doesn't have any meaning in terms of the transactions that @@ -1116,35 +1102,29 @@ impl MinerService for Miner { // Then import all transactions... { - let out_of_chain = retracted - .par_iter() - .map(|h| fetch_transactions(chain, h)); - out_of_chain.for_each(|txs| { - let mut transaction_queue = self.transaction_queue.lock(); - let _ = self.add_transactions_to_queue( - chain, txs, TransactionOrigin::RetractedBlock, &mut transaction_queue - ); - }); + retracted.par_iter() + .map(|hash| { + let block = chain.block(BlockId::Hash(*hash)) + .expect("Client is sending message after commit to db and inserting to chain; the block is available; qed"); + let block = BlockView::new(&block); + let txs = block.transactions(); + // populate sender + for tx in &txs { + let _sender = tx.sender(); + } + txs + }).for_each(|txs| { + let mut transaction_queue = self.transaction_queue.lock(); + let _ = self.add_transactions_to_queue( + chain, txs, TransactionOrigin::RetractedBlock, &mut transaction_queue + ); + }); } - // ...and at the end remove old ones + // ...and at the end remove the old ones { - let in_chain = enacted - .par_iter() - .map(|h: &H256| fetch_transactions(chain, h)); - - in_chain.for_each(|mut txs| { - let mut transaction_queue = self.transaction_queue.lock(); - - let to_remove = txs.drain(..) - .map(|tx| { - tx.sender().expect("Transaction is in block, so sender has to be defined.") - }) - .collect::>(); - for sender in to_remove { - transaction_queue.remove_all(sender, chain.latest_nonce(&sender)); - } - }); + let mut transaction_queue = self.transaction_queue.lock(); + transaction_queue.remove_old(|sender| chain.latest_nonce(sender)); } if enacted.len() > 0 { diff --git a/ethcore/src/miner/transaction_queue.rs b/ethcore/src/miner/transaction_queue.rs index cd2d3ba47..b4cc93a9c 100644 --- a/ethcore/src/miner/transaction_queue.rs +++ b/ethcore/src/miner/transaction_queue.rs @@ -79,8 +79,10 @@ //! we check if the transactions should go to `current` (comparing state nonce) //! - When it's removed from `current` - all transactions from this sender (`current` & `future`) are recalculated. //! 3. `remove_all` is used to inform the queue about client (state) nonce changes. -//! - It removes all transactions (either from `current` or `future`) with nonce < client nonce -//! - It moves matching `future` transactions to `current` +//! - It removes all transactions (either from `current` or `future`) with nonce < client nonce +//! - It moves matching `future` transactions to `current` +//! 4. `remove_old` is used as convenient method to update the state nonce for all senders in the queue. +//! - Invokes `remove_all` with latest state nonce for all senders. use std::ops::Deref; use std::cmp::Ordering; @@ -752,6 +754,26 @@ impl TransactionQueue { /// Removes all transactions from particular sender up to (excluding) given client (state) nonce. /// Client (State) Nonce = next valid nonce for this sender. pub fn remove_all(&mut self, sender: Address, client_nonce: U256) { + // Check if there is anything in current... + let should_check_in_current = self.current.by_address.row(&sender) + // If nonce == client_nonce nothing is changed + .and_then(|by_nonce| by_nonce.keys().find(|nonce| *nonce < &client_nonce)) + .map(|_| ()); + // ... or future + let should_check_in_future = self.future.by_address.row(&sender) + // if nonce == client_nonce we need to promote to current + .and_then(|by_nonce| by_nonce.keys().find(|nonce| *nonce <= &client_nonce)) + .map(|_| ()); + + if should_check_in_current.or(should_check_in_future).is_none() { + return; + } + + self.remove_all_internal(sender, client_nonce); + } + + /// Always updates future and moves transactions from current to future. + fn remove_all_internal(&mut self, sender: Address, client_nonce: U256) { // We will either move transaction to future or remove it completely // so there will be no transactions from this sender in current self.last_nonces.remove(&sender); @@ -765,6 +787,20 @@ impl TransactionQueue { assert_eq!(self.future.by_priority.len() + self.current.by_priority.len(), self.by_hash.len()); } + /// Checks the current nonce for all transactions' senders in the queue and removes the old transactions. + pub fn remove_old(&mut self, fetch_nonce: F) where + F: Fn(&Address) -> U256, + { + let senders = self.current.by_address.keys() + .chain(self.future.by_address.keys()) + .cloned() + .collect::>(); + + for sender in senders { + self.remove_all(sender, fetch_nonce(&sender)); + } + } + /// Penalize transactions from sender of transaction with given hash. /// I.e. it should change the priority of the transaction in the queue. /// @@ -847,7 +883,7 @@ impl TransactionQueue { if order.is_some() { // This will keep consistency in queue // Moves all to future and then promotes a batch from current: - self.remove_all(sender, current_nonce); + self.remove_all_internal(sender, current_nonce); assert_eq!(self.future.by_priority.len() + self.current.by_priority.len(), self.by_hash.len()); return; } @@ -2438,7 +2474,7 @@ mod test { } #[test] - fn should_reject_transactions_below_bas_gas() { + fn should_reject_transactions_below_base_gas() { // given let mut txq = TransactionQueue::default(); let (tx1, tx2) = new_tx_pair_default(1.into(), 0.into()); @@ -2457,4 +2493,26 @@ mod test { } + #[test] + fn should_clear_all_old_transactions() { + // given + let mut txq = TransactionQueue::default(); + let (tx1, tx2) = new_tx_pair_default(1.into(), 0.into()); + let (tx3, tx4) = new_tx_pair_default(1.into(), 0.into()); + let nonce1 = tx1.nonce; + + // Insert all transactions + txq.add(tx1, TransactionOrigin::External, &default_account_details, &gas_estimator).unwrap(); + txq.add(tx2, TransactionOrigin::External, &default_account_details, &gas_estimator).unwrap(); + txq.add(tx3, TransactionOrigin::External, &default_account_details, &gas_estimator).unwrap(); + txq.add(tx4, TransactionOrigin::External, &default_account_details, &gas_estimator).unwrap(); + assert_eq!(txq.top_transactions().len(), 4); + + // when + txq.remove_old(|_| nonce1 + U256::one()); + + // then + assert_eq!(txq.top_transactions().len(), 2); + } + } diff --git a/js/package.json b/js/package.json index 524c280dc..29d7464be 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.105", + "version": "0.2.107", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", @@ -146,6 +146,7 @@ "mobx-react-devtools": "4.2.10", "moment": "2.17.0", "phoneformat.js": "1.0.3", + "push.js": "0.0.11", "qs": "6.3.0", "react": "15.4.1", "react-ace": "4.1.0", diff --git a/js/src/index.js b/js/src/index.js index 6938a46f8..46d6c9c74 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -67,7 +67,7 @@ if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) { const api = new SecureApi(`ws://${parityUrl}`, token); ContractInstances.create(api); -const store = initStore(api); +const store = initStore(api, hashHistory); store.dispatch({ type: 'initAll', api }); store.dispatch(setApi(api)); diff --git a/js/src/redux/middleware.js b/js/src/redux/middleware.js index bb11cf32f..14bc9b0a6 100644 --- a/js/src/redux/middleware.js +++ b/js/src/redux/middleware.js @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . import thunk from 'redux-thunk'; +import { routerMiddleware } from 'react-router-redux'; import ErrorsMiddleware from '~/ui/Errors/middleware'; import SettingsMiddleware from '~/views/Settings/middleware'; @@ -22,12 +23,13 @@ import SignerMiddleware from './providers/signerMiddleware'; import statusMiddleware from '~/views/Status/middleware'; import CertificationsMiddleware from './providers/certifications/middleware'; -export default function (api) { +export default function (api, browserHistory) { const errors = new ErrorsMiddleware(); const signer = new SignerMiddleware(api); const settings = new SettingsMiddleware(); const status = statusMiddleware(); const certifications = new CertificationsMiddleware(); + const routeMiddleware = routerMiddleware(browserHistory); const middleware = [ settings.toMiddleware(), @@ -36,5 +38,5 @@ export default function (api) { certifications.toMiddleware() ]; - return middleware.concat(status, thunk); + return middleware.concat(status, routeMiddleware, thunk); } diff --git a/js/src/redux/providers/balancesActions.js b/js/src/redux/providers/balancesActions.js index 65f831008..f8cfb2c1e 100644 --- a/js/src/redux/providers/balancesActions.js +++ b/js/src/redux/providers/balancesActions.js @@ -15,11 +15,14 @@ // along with Parity. If not, see . import { range, uniq, isEqual } from 'lodash'; +import BigNumber from 'bignumber.js'; +import { push } from 'react-router-redux'; import { hashToImageUrl } from './imagesReducer'; import { setAddressImage } from './imagesActions'; import * as ABIS from '~/contracts/abi'; +import { notifyTransaction } from '~/util/notifications'; import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png'; const ETH = { @@ -28,7 +31,64 @@ const ETH = { image: imagesEthereum }; -export function setBalances (balances) { +function setBalances (_balances) { + return (dispatch, getState) => { + const state = getState(); + + const accounts = state.personal.accounts; + const nextBalances = _balances; + const prevBalances = state.balances.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 { + const oldValue = nextTokens[tokenIndex].value; + + // If received a token/eth (old value < new value), notify + if (oldValue.lt(value) && accounts[address]) { + const account = accounts[address]; + const txValue = value.minus(oldValue); + + const redirectToAccount = () => { + const route = `/account/${account.address}`; + dispatch(push(route)); + }; + + notifyTransaction(account, token, txValue, redirectToAccount); + } + + nextTokens[tokenIndex] = { token, value }; + } + }); + + balances[address] = { txCount: txCount || new BigNumber(0), tokens: nextTokens }; + }); + + dispatch(_setBalances(balances)); + }; +} + +function _setBalances (balances) { return { type: 'setBalances', balances @@ -123,14 +183,14 @@ export function fetchBalances (_addresses) { const fullFetch = addresses.length === 1; - const fetchedAddresses = uniq(addresses.concat(Object.keys(accounts))); + const addressesToFetch = uniq(addresses.concat(Object.keys(accounts))); return Promise - .all(fetchedAddresses.map((addr) => fetchAccount(addr, api, fullFetch))) + .all(addressesToFetch.map((addr) => fetchAccount(addr, api, fullFetch))) .then((accountsBalances) => { const balances = {}; - fetchedAddresses.forEach((addr, idx) => { + addressesToFetch.forEach((addr, idx) => { balances[addr] = accountsBalances[idx]; }); @@ -146,10 +206,12 @@ export function fetchBalances (_addresses) { export function updateTokensFilter (_addresses, _tokens) { return (dispatch, getState) => { const { api, balances, personal } = getState(); - const { visibleAccounts } = personal; + const { visibleAccounts, accounts } = personal; const { tokensFilter } = balances; - const addresses = uniq(_addresses || visibleAccounts || []).sort(); + const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts))); + const addresses = uniq(_addresses || addressesToFetch || []).sort(); + const tokens = _tokens || Object.values(balances.tokens) || []; const tokenAddresses = tokens.map((t) => t.address).sort(); @@ -221,8 +283,10 @@ export function updateTokensFilter (_addresses, _tokens) { export function queryTokensFilter (tokensFilter) { return (dispatch, getState) => { const { api, personal, balances } = getState(); - const { visibleAccounts } = personal; + const { visibleAccounts, accounts } = personal; + const visibleAddresses = visibleAccounts.map((a) => a.toLowerCase()); + const addressesToFetch = uniq(visibleAddresses.concat(Object.keys(accounts))); Promise .all([ @@ -237,18 +301,16 @@ export function queryTokensFilter (tokensFilter) { .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 (addressesToFetch.includes(fromAddress)) { + addresses.push(fromAddress); } - if (toIdx > -1) { - addresses.push(visibleAccounts[toIdx]); + if (addressesToFetch.includes(toAddress)) { + addresses.push(toAddress); } tokenAddresses.push(tokenAddress); @@ -269,9 +331,10 @@ export function queryTokensFilter (tokensFilter) { export function fetchTokensBalances (_addresses = null, _tokens = null) { return (dispatch, getState) => { const { api, personal, balances } = getState(); - const { visibleAccounts } = personal; + const { visibleAccounts, accounts } = personal; - const addresses = _addresses || visibleAccounts; + const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts))); + const addresses = _addresses || addressesToFetch; const tokens = _tokens || Object.values(balances.tokens); if (addresses.length === 0) { diff --git a/js/src/redux/providers/balancesReducer.js b/js/src/redux/providers/balancesReducer.js index e21ae676d..4b6950498 100644 --- a/js/src/redux/providers/balancesReducer.js +++ b/js/src/redux/providers/balancesReducer.js @@ -15,7 +15,6 @@ // along with Parity. If not, see . import { handleActions } from 'redux-actions'; -import BigNumber from 'bignumber.js'; const initialState = { balances: {}, @@ -26,39 +25,7 @@ const initialState = { export default handleActions({ 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 && 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 }); - }); - + const { balances } = action; return Object.assign({}, state, { balances }); }, diff --git a/js/src/redux/providers/index.js b/js/src/redux/providers/index.js index 563378caa..a90d8b62c 100644 --- a/js/src/redux/providers/index.js +++ b/js/src/redux/providers/index.js @@ -21,11 +21,11 @@ export Status from './status'; export apiReducer from './apiReducer'; export balancesReducer from './balancesReducer'; +export blockchainReducer from './blockchainReducer'; +export compilerReducer from './compilerReducer'; export imagesReducer from './imagesReducer'; export personalReducer from './personalReducer'; export signerReducer from './signerReducer'; -export statusReducer from './statusReducer'; -export blockchainReducer from './blockchainReducer'; -export compilerReducer from './compilerReducer'; export snackbarReducer from './snackbarReducer'; +export statusReducer from './statusReducer'; export walletReducer from './walletReducer'; diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index 830192bbe..138479716 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -54,7 +54,10 @@ export default class Status { this._api.eth .getBlockByNumber(blockNumber) .then((block) => { - this._store.dispatch(statusCollection({ gasLimit: block.gasLimit })); + this._store.dispatch(statusCollection({ + blockTimestamp: block.timestamp, + gasLimit: block.gasLimit + })); }) .catch((error) => { console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error); diff --git a/js/src/redux/providers/statusReducer.js b/js/src/redux/providers/statusReducer.js index 07ba4af5b..279a9da42 100644 --- a/js/src/redux/providers/statusReducer.js +++ b/js/src/redux/providers/statusReducer.js @@ -19,6 +19,7 @@ import { handleActions } from 'redux-actions'; const initialState = { blockNumber: new BigNumber(0), + blockTimestamp: new Date(), devLogs: [], devLogsLevels: null, devLogsEnabled: false, diff --git a/js/src/redux/reducers.js b/js/src/redux/reducers.js index 92388df65..642dfe403 100644 --- a/js/src/redux/reducers.js +++ b/js/src/redux/reducers.js @@ -17,7 +17,12 @@ import { combineReducers } from 'redux'; import { routerReducer } from 'react-router-redux'; -import { apiReducer, balancesReducer, blockchainReducer, compilerReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer, snackbarReducer, walletReducer } from './providers'; +import { + apiReducer, balancesReducer, blockchainReducer, + compilerReducer, imagesReducer, personalReducer, + signerReducer, statusReducer as nodeStatusReducer, + snackbarReducer, walletReducer +} from './providers'; import certificationsReducer from './providers/certifications/reducer'; import errorReducer from '~/ui/Errors/reducers'; diff --git a/js/src/redux/store.js b/js/src/redux/store.js index 2ff50ea53..1d62f9ea5 100644 --- a/js/src/redux/store.js +++ b/js/src/redux/store.js @@ -32,9 +32,9 @@ const storeCreation = window.devToolsExtension ? window.devToolsExtension()(createStore) : createStore; -export default function (api) { +export default function (api, browserHistory) { const reducers = initReducers(); - const middleware = initMiddleware(api); + const middleware = initMiddleware(api, browserHistory); const store = applyMiddleware(...middleware)(storeCreation)(reducers); new BalancesProvider(store, api).start(); diff --git a/js/src/ui/Actionbar/actionbar.js b/js/src/ui/Actionbar/actionbar.js index c3fbe6a18..f744f5c57 100644 --- a/js/src/ui/Actionbar/actionbar.js +++ b/js/src/ui/Actionbar/actionbar.js @@ -17,11 +17,13 @@ import React, { Component, PropTypes } from 'react'; import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar'; +import { nodeOrStringProptype } from '~/util/proptypes'; + import styles from './actionbar.css'; export default class Actionbar extends Component { static propTypes = { - title: PropTypes.string, + title: nodeOrStringProptype(), buttons: PropTypes.array, children: PropTypes.node, className: PropTypes.string diff --git a/js/src/ui/Page/page.css b/js/src/ui/Page/page.css index ace070528..06d4f7274 100644 --- a/js/src/ui/Page/page.css +++ b/js/src/ui/Page/page.css @@ -18,7 +18,7 @@ .layout { padding: 0.25em; - > * { + &>div { margin-bottom: 0.75em; } } diff --git a/js/src/ui/Page/page.js b/js/src/ui/Page/page.js index 867c3fdf1..9646358b3 100644 --- a/js/src/ui/Page/page.js +++ b/js/src/ui/Page/page.js @@ -16,21 +16,38 @@ import React, { Component, PropTypes } from 'react'; +import Actionbar from '../Actionbar'; +import { nodeOrStringProptype } from '~/util/proptypes'; + import styles from './page.css'; export default class Page extends Component { static propTypes = { + buttons: PropTypes.array, className: PropTypes.string, - children: PropTypes.node + children: PropTypes.node, + title: nodeOrStringProptype() }; render () { - const { className, children } = this.props; + const { buttons, className, children, title } = this.props; const classes = `${styles.layout} ${className}`; + let actionbar = null; + + if (title || buttons) { + actionbar = ( + + ); + } return ( -
- { children } +
+ { actionbar } +
+ { children } +
); } diff --git a/js/src/util/notifications.js b/js/src/util/notifications.js new file mode 100644 index 000000000..479448234 --- /dev/null +++ b/js/src/util/notifications.js @@ -0,0 +1,45 @@ +// 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 Push from 'push.js'; +import BigNumber from 'bignumber.js'; +import { noop } from 'lodash'; + +import { fromWei } from '~/api/util/wei'; + +import ethereumIcon from '~/../assets/images/contracts/ethereum-black-64x64.png'; +import unkownIcon from '~/../assets/images/contracts/unknown-64x64.png'; + +export function notifyTransaction (account, token, _value, onClick) { + const name = account.name || account.address; + const value = token.tag.toLowerCase() === 'eth' + ? fromWei(_value) + : _value.div(new BigNumber(token.format || 1)); + + const icon = token.tag.toLowerCase() === 'eth' + ? ethereumIcon + : (token.image || unkownIcon); + + Push.create(`${name}`, { + body: `You just received ${value.toFormat()} ${token.tag.toUpperCase()}`, + icon: { + x16: icon, + x32: icon + }, + timeout: 20000, + onClick: onClick || noop + }); +} diff --git a/js/src/views/Dapp/dapp.js b/js/src/views/Dapp/dapp.js index 9094e4dc3..3e9218914 100644 --- a/js/src/views/Dapp/dapp.js +++ b/js/src/views/Dapp/dapp.js @@ -51,9 +51,16 @@ export default class Dapp extends Component { src = `${dappsUrl}/${app.contentHash}/`; break; default: - const dapphost = process.env.NODE_ENV === 'production' && !app.secure - ? `${dappsUrl}/ui` - : ''; + let dapphost = process.env.DAPPS_URL || ( + process.env.NODE_ENV === 'production' && !app.secure + ? `${dappsUrl}/ui` + : '' + ); + + if (dapphost === '/') { + dapphost = ''; + } + src = `${dapphost}/${app.url}.html`; break; } diff --git a/js/src/views/Signer/signer.js b/js/src/views/Signer/signer.js index 6d68c1dc0..f0b4baed7 100644 --- a/js/src/views/Signer/signer.js +++ b/js/src/views/Signer/signer.js @@ -23,8 +23,7 @@ export default class Signer extends Component { render () { return (
- +
); diff --git a/js/src/views/Signer/store.js b/js/src/views/Signer/store.js index 0eeb99861..bcc85a431 100644 --- a/js/src/views/Signer/store.js +++ b/js/src/views/Signer/store.js @@ -30,12 +30,6 @@ export default class Store { } } - @action unsubscribe () { - if (this._timeoutId) { - clearTimeout(this._timeoutId); - } - } - @action setBalance = (address, balance) => { this.setBalances({ [address]: balance }); } @@ -50,6 +44,12 @@ export default class Store { } } + @action unsubscribe () { + if (this._timeoutId) { + clearTimeout(this._timeoutId); + } + } + fetchBalance (address) { this._api.eth .getBalance(address) diff --git a/js/src/views/Status/components/Debug/Debug.css b/js/src/views/Status/components/Debug/debug.css similarity index 100% rename from js/src/views/Status/components/Debug/Debug.css rename to js/src/views/Status/components/Debug/debug.css diff --git a/js/src/views/Status/components/Debug/Debug.js b/js/src/views/Status/components/Debug/debug.js similarity index 99% rename from js/src/views/Status/components/Debug/Debug.js rename to js/src/views/Status/components/Debug/debug.js index 84ce1bb87..438d208f9 100644 --- a/js/src/views/Status/components/Debug/Debug.js +++ b/js/src/views/Status/components/Debug/debug.js @@ -22,7 +22,7 @@ import ReorderIcon from 'material-ui/svg-icons/action/reorder'; import { Container } from '~/ui'; -import styles from './Debug.css'; +import styles from './debug.css'; export default class Debug extends Component { static propTypes = { diff --git a/js/src/views/Status/components/Debug/index.js b/js/src/views/Status/components/Debug/index.js index 9e8eae219..c6b522c46 100644 --- a/js/src/views/Status/components/Debug/index.js +++ b/js/src/views/Status/components/Debug/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './Debug'; +export default from './debug'; diff --git a/js/src/views/Status/components/MiningSettings/index.js b/js/src/views/Status/components/MiningSettings/index.js index 3eee1e051..7853e0d33 100644 --- a/js/src/views/Status/components/MiningSettings/index.js +++ b/js/src/views/Status/components/MiningSettings/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './MiningSettings'; +export default from './miningSettings'; diff --git a/js/src/views/Status/components/MiningSettings/MiningSettings.js b/js/src/views/Status/components/MiningSettings/miningSettings.js similarity index 100% rename from js/src/views/Status/components/MiningSettings/MiningSettings.js rename to js/src/views/Status/components/MiningSettings/miningSettings.js diff --git a/js/src/views/Status/components/Status/index.js b/js/src/views/Status/components/Status/index.js index 8885a04c6..44079b224 100644 --- a/js/src/views/Status/components/Status/index.js +++ b/js/src/views/Status/components/Status/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './Status'; +export default from './status'; diff --git a/js/src/views/Status/components/Status/Status.css b/js/src/views/Status/components/Status/status.css similarity index 90% rename from js/src/views/Status/components/Status/Status.css rename to js/src/views/Status/components/Status/status.css index d79d41062..229e72ce1 100644 --- a/js/src/views/Status/components/Status/Status.css +++ b/js/src/views/Status/components/Status/status.css @@ -28,10 +28,19 @@ content: ''; } -.blockinfo { - font-size: 24px; +.blockInfo { color: #aaa; - line-height: 24px; + font-size: 1.5em; + line-height: 1.5em; +} + +.blockByline { + color: #aaa; + font-size: 0.75em; +} + +.padBottom { + padding-bottom: 1.25em !important; } .col, diff --git a/js/src/views/Status/components/Status/Status.js b/js/src/views/Status/components/Status/status.js similarity index 84% rename from js/src/views/Status/components/Status/Status.js rename to js/src/views/Status/components/Status/status.js index e2d65cb9a..891ec6632 100644 --- a/js/src/views/Status/components/Status/Status.js +++ b/js/src/views/Status/components/Status/status.js @@ -14,14 +14,16 @@ // 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 bytes from 'bytes'; +import moment from 'moment'; +import React, { Component, PropTypes } from 'react'; import { Container, ContainerTitle, Input } from '~/ui'; -import styles from './Status.css'; import MiningSettings from '../MiningSettings'; +import styles from './status.css'; + export default class Status extends Component { static propTypes = { nodeStatus: PropTypes.object.isRequired, @@ -44,23 +46,26 @@ export default class Status extends Component {
-
+
-

+
#{ nodeStatus.blockNumber.toFormat() } -

+
+
+ { moment().calendar(nodeStatus.blockTimestamp) } +
-
+
-

+
{ peers } -

+
-
+
-

+
{ `${hashrate} H/s` } -

+
diff --git a/js/src/views/Status/containers/StatusPage/index.js b/js/src/views/Status/containers/StatusPage/index.js index e36bc949c..0e141a16d 100644 --- a/js/src/views/Status/containers/StatusPage/index.js +++ b/js/src/views/Status/containers/StatusPage/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './StatusPage'; +export default from './statusPage'; diff --git a/js/src/views/Status/status.css b/js/src/views/Status/containers/StatusPage/statusPage.css similarity index 93% rename from js/src/views/Status/status.css rename to js/src/views/Status/containers/StatusPage/statusPage.css index 0ed554ae6..bda975813 100644 --- a/js/src/views/Status/status.css +++ b/js/src/views/Status/containers/StatusPage/statusPage.css @@ -14,5 +14,9 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see . */ -.container { + +.body { + &>div { + margin-bottom: 0.25em; + } } diff --git a/js/src/views/Status/containers/StatusPage/StatusPage.js b/js/src/views/Status/containers/StatusPage/statusPage.js similarity index 95% rename from js/src/views/Status/containers/StatusPage/StatusPage.js rename to js/src/views/Status/containers/StatusPage/statusPage.js index c7ab56c5e..286e2c20e 100644 --- a/js/src/views/Status/containers/StatusPage/StatusPage.js +++ b/js/src/views/Status/containers/StatusPage/statusPage.js @@ -23,6 +23,8 @@ import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from '~/redux/ import Debug from '../../components/Debug'; import Status from '../../components/Status'; +import styles from './statusPage.css'; + class StatusPage extends Component { static propTypes = { nodeStatus: PropTypes.object.isRequired, @@ -39,7 +41,7 @@ class StatusPage extends Component { render () { return ( -
+
diff --git a/js/src/views/Status/status.js b/js/src/views/Status/status.js index ff13f1a07..805c96850 100644 --- a/js/src/views/Status/status.js +++ b/js/src/views/Status/status.js @@ -16,22 +16,16 @@ import React, { Component } from 'react'; -import { Actionbar, Page } from '~/ui'; +import { Page } from '~/ui'; import StatusPage from './containers/StatusPage'; -import styles from './status.css'; - export default class Status extends Component { render () { return ( -
- - - - -
+ + + ); } } diff --git a/js/webpack/app.js b/js/webpack/app.js index 5998cf30b..a7b086b96 100644 --- a/js/webpack/app.js +++ b/js/webpack/app.js @@ -20,6 +20,7 @@ const path = require('path'); const WebpackErrorNotificationPlugin = require('webpack-error-notification'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); const Shared = require('./shared'); const DAPPS = require('../src/dapps'); @@ -41,7 +42,7 @@ module.exports = { output: { publicPath: '/', path: path.join(__dirname, '../', DEST), - filename: '[name].[hash].js' + filename: '[name].[hash:10].js' }, module: { @@ -85,13 +86,20 @@ module.exports = { { test: /\.css$/, include: [ /src/ ], + // exclude: [ /src\/dapps/ ], + loader: isProd ? ExtractTextPlugin.extract([ + // 'style-loader', + 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', + 'postcss-loader' + ]) : undefined, // use: [ 'happypack/loader?id=css' ] - use: [ + use: isProd ? undefined : [ 'style-loader', 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 'postcss-loader' ] }, + { test: /\.css$/, exclude: [ /src/ ], @@ -99,11 +107,15 @@ module.exports = { }, { test: /\.(png|jpg)$/, - use: [ 'file-loader?name=[name].[hash].[ext]' ] + use: [ 'file-loader?&name=assets/[name].[hash:10].[ext]' ] }, { - test: /\.(woff(2)|ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/, - use: [ 'file-loader' ] + test: /\.(woff(2)|ttf|eot|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/, + use: [ 'file-loader?name=fonts/[name][hash:10].[ext]' ] + }, + { + test: /\.svg(\?v=[0-9]\.[0-9]\.[0-9])?$/, + use: [ 'file-loader?name=assets/[name].[hash:10].[ext]' ] } ], noParse: [ @@ -153,13 +165,20 @@ module.exports = { if (!isProd) { plugins.push( new webpack.optimize.CommonsChunkPlugin({ - filename: 'commons.[hash].js', + filename: 'commons.[hash:10].js', name: 'commons', minChunks: Infinity }) ); } + if (isProd) { + plugins.push(new ExtractTextPlugin({ + filename: 'styles/[name].[hash:10].css', + allChunks: true + })); + } + return plugins; }()) }; diff --git a/util/table/src/lib.rs b/util/table/src/lib.rs index a13beab11..f65c1e171 100644 --- a/util/table/src/lib.rs +++ b/util/table/src/lib.rs @@ -18,6 +18,7 @@ use std::hash::Hash; use std::collections::HashMap; +use std::collections::hash_map::Keys; /// Structure to hold double-indexed values /// @@ -41,6 +42,11 @@ impl Table } } + /// Returns keys iterator for this Table. + pub fn keys(&self) -> Keys> { + self.map.keys() + } + /// Removes all elements from this Table pub fn clear(&mut self) { self.map.clear();