diff --git a/dapps/src/api/types.rs.in b/dapps/src/api/types.rs.in index 8bbefaa83..a95a0d446 100644 --- a/dapps/src/api/types.rs.in +++ b/dapps/src/api/types.rs.in @@ -17,6 +17,7 @@ use endpoint::EndpointInfo; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct App { pub id: String, pub name: String, @@ -54,6 +55,7 @@ impl Into for App { } #[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct ApiError { pub code: String, pub title: String, diff --git a/docker/ubuntu-stable/Dockerfile b/docker/ubuntu-stable/Dockerfile index 2a8ee3da8..3fe36f32c 100644 --- a/docker/ubuntu-stable/Dockerfile +++ b/docker/ubuntu-stable/Dockerfile @@ -3,6 +3,7 @@ WORKDIR /build # install tools and dependencies RUN apt-get update && \ apt-get install -y \ + build-essential \ g++ \ curl \ git \ diff --git a/docker/ubuntu/Dockerfile b/docker/ubuntu/Dockerfile index 0c8e7d5db..e98c60daa 100644 --- a/docker/ubuntu/Dockerfile +++ b/docker/ubuntu/Dockerfile @@ -4,6 +4,7 @@ WORKDIR /build RUN apt-get update && \ apt-get install -y \ g++ \ + build-essential \ curl \ git \ file \ diff --git a/js/src/modals/DeployContract/deployContract.js b/js/src/modals/DeployContract/deployContract.js index 55e3166e8..7a4979489 100644 --- a/js/src/modals/DeployContract/deployContract.js +++ b/js/src/modals/DeployContract/deployContract.js @@ -128,7 +128,7 @@ export default class DeployContract extends Component { title={ title } waiting={ waiting } visible - scroll> + > { this.renderStep() } ); diff --git a/js/src/modals/LoadContract/loadContract.js b/js/src/modals/LoadContract/loadContract.js index e0e4127e3..b530f601a 100644 --- a/js/src/modals/LoadContract/loadContract.js +++ b/js/src/modals/LoadContract/loadContract.js @@ -62,7 +62,6 @@ export default class LoadContract extends Component { title={ title } actions={ this.renderDialogActions() } visible - scroll > { this.renderBody() } diff --git a/js/src/modals/SMSVerification/SMSVerification.js b/js/src/modals/SMSVerification/SMSVerification.js index 62e7e992b..7bf7d67ae 100644 --- a/js/src/modals/SMSVerification/SMSVerification.js +++ b/js/src/modals/SMSVerification/SMSVerification.js @@ -61,7 +61,7 @@ export default class SMSVerification extends Component { { this.renderWarning() } { this.renderPage() } diff --git a/js/src/ui/Container/container.js b/js/src/ui/Container/container.js index 143115f45..55b0b838e 100644 --- a/js/src/ui/Container/container.js +++ b/js/src/ui/Container/container.js @@ -17,6 +17,8 @@ import React, { Component, PropTypes } from 'react'; import { Card } from 'material-ui/Card'; +import Title from './Title'; + import styles from './container.css'; export default class Container extends Component { @@ -25,7 +27,10 @@ export default class Container extends Component { className: PropTypes.string, compact: PropTypes.bool, light: PropTypes.bool, - style: PropTypes.object + style: PropTypes.object, + title: PropTypes.oneOfType([ + PropTypes.string, PropTypes.node + ]) } render () { @@ -35,9 +40,22 @@ export default class Container extends Component { return (
+ { this.renderTitle() } { children }
); } + + renderTitle () { + const { title } = this.props; + + if (!title) { + return null; + } + + return ( + + ); + } } diff --git a/js/src/ui/Modal/modal.js b/js/src/ui/Modal/modal.js index 7c2a54b4e..4e036a1e1 100644 --- a/js/src/ui/Modal/modal.js +++ b/js/src/ui/Modal/modal.js @@ -41,7 +41,6 @@ class Modal extends Component { compact: PropTypes.bool, current: PropTypes.number, waiting: PropTypes.array, - scroll: PropTypes.bool, steps: PropTypes.array, title: PropTypes.oneOfType([ PropTypes.node, PropTypes.string @@ -52,7 +51,7 @@ class Modal extends Component { render () { const { muiTheme } = this.context; - const { actions, busy, className, current, children, compact, scroll, steps, waiting, title, visible, settings } = this.props; + const { actions, busy, className, current, children, compact, steps, waiting, title, visible, settings } = this.props; const contentStyle = muiTheme.parity.getBackgroundStyle(null, settings.backgroundSeed); const header = ( <Title @@ -70,7 +69,7 @@ class Modal extends Component { actions={ actions } actionsContainerStyle={ ACTIONS_STYLE } autoDetectWindowHeight={ false } - autoScrollBodyContent={ !!scroll } + autoScrollBodyContent actionsContainerClassName={ styles.actions } bodyClassName={ styles.body } contentClassName={ styles.content } diff --git a/js/src/views/Account/Transactions/Transaction/index.js b/js/src/ui/TxList/index.js similarity index 94% rename from js/src/views/Account/Transactions/Transaction/index.js rename to js/src/ui/TxList/index.js index 28a39ad46..820d4b3aa 100644 --- a/js/src/views/Account/Transactions/Transaction/index.js +++ b/js/src/ui/TxList/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -export default from './transaction'; +export default from './txList'; diff --git a/js/src/ui/TxList/store.js b/js/src/ui/TxList/store.js new file mode 100644 index 000000000..ab35d5468 --- /dev/null +++ b/js/src/ui/TxList/store.js @@ -0,0 +1,131 @@ +// 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 <http://www.gnu.org/licenses/>. + +import { action, observable, transaction } from 'mobx'; +import { uniq } from 'lodash'; + +export default class Store { + @observable blocks = {}; + @observable sortedHashes = []; + @observable transactions = {}; + + constructor (api) { + this._api = api; + this._subscriptionId = 0; + this._pendingHashes = []; + + this.subscribe(); + } + + @action addBlocks = (blocks) => { + this.blocks = Object.assign({}, this.blocks, blocks); + } + + @action addTransactions = (transactions) => { + transaction(() => { + this.transactions = Object.assign({}, this.transactions, transactions); + this.sortedHashes = Object + .keys(this.transactions) + .sort((ahash, bhash) => { + const bnA = this.transactions[ahash].blockNumber; + const bnB = this.transactions[bhash].blockNumber; + + if (bnB.eq(0)) { + return bnB.eq(bnA) ? 0 : 1; + } + + return bnB.comparedTo(bnA); + }); + this._pendingHashes = this.sortedHashes.filter((hash) => this.transactions[hash].blockNumber.eq(0)); + }); + } + + @action clearPending () { + this._pendingHashes = []; + } + + subscribe () { + this._api + .subscribe('eth_blockNumber', (error, blockNumber) => { + if (error) { + return; + } + + if (this._pendingHashes.length) { + this.loadTransactions(this._pendingHashes); + this.clearPending(); + } + }) + .then((subscriptionId) => { + this._subscriptionId = subscriptionId; + }); + } + + unsubscribe () { + if (!this._subscriptionId) { + return; + } + + this._api.unsubscribe(this._subscriptionId); + this._subscriptionId = 0; + } + + loadTransactions (_txhashes) { + const txhashes = _txhashes.filter((hash) => !this.transactions[hash] || this._pendingHashes.includes(hash)); + + if (!txhashes || !txhashes.length) { + return; + } + + Promise + .all(txhashes.map((txhash) => this._api.eth.getTransactionByHash(txhash))) + .then((transactions) => { + this.addTransactions( + transactions.reduce((transactions, tx, index) => { + transactions[txhashes[index]] = tx; + return transactions; + }, {}) + ); + + this.loadBlocks(transactions.map((tx) => tx.blockNumber ? tx.blockNumber.toNumber() : 0)); + }) + .catch((error) => { + console.warn('loadTransactions', error); + }); + } + + loadBlocks (_blockNumbers) { + const blockNumbers = uniq(_blockNumbers).filter((bn) => !this.blocks[bn]); + + if (!blockNumbers || !blockNumbers.length) { + return; + } + + Promise + .all(blockNumbers.map((blockNumber) => this._api.eth.getBlockByNumber(blockNumber))) + .then((blocks) => { + this.addBlocks( + blocks.reduce((blocks, block, index) => { + blocks[blockNumbers[index]] = block; + return blocks; + }, {}) + ); + }) + .catch((error) => { + console.warn('loadBlocks', error); + }); + } +} diff --git a/js/src/ui/TxList/txList.css b/js/src/ui/TxList/txList.css new file mode 100644 index 000000000..ebe672bf8 --- /dev/null +++ b/js/src/ui/TxList/txList.css @@ -0,0 +1,81 @@ +/* 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 <http://www.gnu.org/licenses/>. +*/ + +.transactions { + width: 100%; + border-collapse: collapse; + + tr { + line-height: 32px; + vertical-align: top; + + &:nth-child(even) { + background: rgba(255, 255, 255, 0.04); + } + } + + th { + color: #aaa; + text-align: center; + } + + td { + vertical-align: top; + padding: 0.75em 0.75em; + + &.method { + width: 40%; + } + + &.timestamp { + padding-top: 1.5em; + text-align: right; + line-height: 1.5em; + opacity: 0.5; + } + + &.transaction { + padding-top: 1.5em; + text-align: center; + + & div { + line-height: 1.25em; + min-height: 1.25em; + } + } + } + + .icon { + margin: 0; + } + + .link { + vertical-align: top; + } + + .right { + text-align: right; + } + + .center { + text-align: center; + } + + .left { + text-align: left; + } +} diff --git a/js/src/ui/TxList/txList.js b/js/src/ui/TxList/txList.js new file mode 100644 index 000000000..5b47c2ca8 --- /dev/null +++ b/js/src/ui/TxList/txList.js @@ -0,0 +1,178 @@ +// 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 <http://www.gnu.org/licenses/>. + +import moment from 'moment'; +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { observer } from 'mobx-react'; + +import { txLink, addressLink } from '../../3rdparty/etherscan/links'; + +import IdentityIcon from '../IdentityIcon'; +import IdentityName from '../IdentityName'; +import MethodDecoding from '../MethodDecoding'; +import Store from './store'; + +import styles from './txList.css'; + +@observer +class TxList extends Component { + static contextTypes = { + api: PropTypes.object.isRequired + } + + static propTypes = { + address: PropTypes.string.isRequired, + hashes: PropTypes.oneOfType([ + PropTypes.array, + PropTypes.object + ]).isRequired, + isTest: PropTypes.bool.isRequired + } + + store = new Store(this.context.api); + + componentWillMount () { + this.store.loadTransactions(this.props.hashes); + } + + componentWillUnmount () { + this.store.unsubscribe(); + } + + componentWillReceiveProps (newProps) { + this.store.loadTransactions(newProps.hashes); + } + + render () { + return ( + <table className={ styles.transactions }> + <tbody> + { this.renderRows() } + </tbody> + </table> + ); + } + + renderRows () { + const { address, isTest } = this.props; + + return this.store.sortedHashes.map((txhash) => { + const tx = this.store.transactions[txhash]; + + return ( + <tr key={ tx.hash }> + { this.renderBlockNumber(tx.blockNumber) } + { this.renderAddress(tx.from) } + <td className={ styles.transaction }> + { this.renderEtherValue(tx.value) } + <div>⇒</div> + <div> + <a + className={ styles.link } + href={ txLink(tx.hash, isTest) } + target='_blank'> + { `${tx.hash.substr(2, 6)}...${tx.hash.slice(-6)}` } + </a> + </div> + </td> + { this.renderAddress(tx.to) } + <td className={ styles.method }> + <MethodDecoding + historic + address={ address } + transaction={ tx } /> + </td> + </tr> + ); + }); + } + + renderAddress (address) { + const { isTest } = this.props; + + let esLink = null; + if (address) { + esLink = ( + <a + href={ addressLink(address, isTest) } + target='_blank' + className={ styles.link }> + <IdentityName address={ address } shorten /> + </a> + ); + } + + return ( + <td className={ styles.address }> + <div className={ styles.center }> + <IdentityIcon + center + className={ styles.icon } + address={ address } /> + </div> + <div className={ styles.center }> + { esLink || 'DEPLOY' } + </div> + </td> + ); + } + + renderEtherValue (_value) { + const { api } = this.context; + const value = api.util.fromWei(_value); + + if (value.eq(0)) { + return <div className={ styles.value }>{ ' ' }</div>; + } + + return ( + <div className={ styles.value }> + { value.toFormat(5) }<small>ETH</small> + </div> + ); + } + + renderBlockNumber (_blockNumber) { + const blockNumber = _blockNumber.toNumber(); + const block = this.store.blocks[blockNumber]; + + return ( + <td className={ styles.timestamp }> + <div>{ blockNumber && block ? moment(block.timestamp).fromNow() : null }</div> + <div>{ blockNumber ? _blockNumber.toFormat() : 'Pending' }</div> + </td> + ); + } +} + +function mapStateToProps (state) { + const { isTest } = state.nodeStatus; + + return { + isTest + }; +} + +function mapDispatchToProps (dispatch) { + return bindActionCreators({}, dispatch); +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(TxList); diff --git a/js/src/ui/index.js b/js/src/ui/index.js index 6824d9887..4c16bc4a2 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -42,6 +42,7 @@ import SignerIcon from './SignerIcon'; import Tags from './Tags'; import Tooltips, { Tooltip } from './Tooltips'; import TxHash from './TxHash'; +import TxList from './TxList'; export { Actionbar, @@ -85,5 +86,6 @@ export { Tags, Tooltip, Tooltips, - TxHash + TxHash, + TxList }; diff --git a/js/src/views/Account/Transactions/Transaction/transaction.js b/js/src/views/Account/Transactions/Transaction/transaction.js deleted file mode 100644 index 32aa406a8..000000000 --- a/js/src/views/Account/Transactions/Transaction/transaction.js +++ /dev/null @@ -1,184 +0,0 @@ -// 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 <http://www.gnu.org/licenses/>. - -import BigNumber from 'bignumber.js'; -import React, { Component, PropTypes } from 'react'; -import moment from 'moment'; - -import { IdentityIcon, IdentityName, MethodDecoding } from '../../../../ui'; -import ShortenedHash from '../../../../ui/ShortenedHash'; -import { txLink, addressLink } from '../../../../3rdparty/etherscan/links'; - -import styles from '../transactions.css'; - -export default class Transaction extends Component { - static contextTypes = { - api: PropTypes.object.isRequired - } - - static propTypes = { - address: PropTypes.string.isRequired, - isTest: PropTypes.bool.isRequired, - transaction: PropTypes.object.isRequired - } - - state = { - isContract: false, - isReceived: false, - transaction: null, - block: null - } - - componentDidMount () { - this.lookup(); - } - - render () { - const { block } = this.state; - const { transaction } = this.props; - - return ( - <tr> - <td className={ styles.timestamp }> - <div>{ this.formatBlockTimestamp(block) }</div> - <div>{ this.formatNumber(transaction.blockNumber) }</div> - </td> - { this.renderAddress(transaction.from) } - { this.renderTransaction() } - { this.renderAddress(transaction.to) } - <td className={ styles.method }> - { this.renderMethod() } - </td> - </tr> - ); - } - - renderMethod () { - const { address } = this.props; - const { transaction } = this.state; - - if (!transaction) { - return null; - } - - return ( - <MethodDecoding - historic - address={ address } - transaction={ transaction } /> - ); - } - - renderTransaction () { - const { isTest } = this.props; - const { transaction } = this.props; - - return ( - <td className={ styles.transaction }> - { this.renderEtherValue() } - <div>⇒</div> - <div> - <a - className={ styles.link } - href={ txLink(transaction.hash, isTest) } - target='_blank' - > - <ShortenedHash data={ transaction.hash } /> - </a> - </div> - </td> - ); - } - - renderAddress (address) { - const { isTest } = this.props; - - const eslink = address ? ( - <a - href={ addressLink(address, isTest) } - target='_blank' - className={ styles.link }> - <IdentityName address={ address } shorten /> - </a> - ) : 'DEPLOY'; - - return ( - <td className={ styles.address }> - <div className={ styles.center }> - <IdentityIcon - center - className={ styles.icon } - address={ address } /> - </div> - <div className={ styles.center }> - { eslink } - </div> - </td> - ); - } - - renderEtherValue () { - const { api } = this.context; - const { transaction } = this.state; - - if (!transaction) { - return null; - } - - const value = api.util.fromWei(transaction.value); - - if (value.eq(0)) { - return <div className={ styles.value }>{ ' ' }</div>; - } - - return ( - <div className={ styles.value }> - { value.toFormat(5) }<small>ETH</small> - </div> - ); - } - - formatNumber (number) { - return new BigNumber(number).toFormat(); - } - - formatBlockTimestamp (block) { - if (!block) { - return null; - } - - return moment(block.timestamp).fromNow(); - } - - lookup () { - const { api } = this.context; - const { transaction, address } = this.props; - - this.setState({ isReceived: address === transaction.to }); - - Promise - .all([ - api.eth.getBlockByNumber(transaction.blockNumber), - api.eth.getTransactionByHash(transaction.hash) - ]) - .then(([block, transaction]) => { - this.setState({ block, transaction }); - }) - .catch((error) => { - console.warn('lookup', error); - }); - } -} diff --git a/js/src/views/Account/Transactions/transactions.css b/js/src/views/Account/Transactions/transactions.css index 62411d8f2..13d727deb 100644 --- a/js/src/views/Account/Transactions/transactions.css +++ b/js/src/views/Account/Transactions/transactions.css @@ -14,49 +14,10 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see <http://www.gnu.org/licenses/>. */ -.right { - text-align: right; -} - -.center { - text-align: center; -} - -.left { - text-align: left; -} .transactions { } -.transactions table { - width: 100%; - border-collapse: collapse; -} - -.transactions tr { - line-height: 32px; - vertical-align: top; -} - -.transactions tr:nth-child(even) { - background: rgba(255, 255, 255, 0.04); -} - -.transactions th { - color: #aaa; - text-align: center; -} - -.transactions td { - vertical-align: top; - padding: 0.75em 0.75em; -} - -.transactions .link { - vertical-align: top; -} - .infonone { opacity: 0.25; } @@ -67,35 +28,3 @@ font-size: 0.75em; color: #aaa; } - -.address { - text-align: center; -} - -.transaction { - text-align: center; -} - -.transactions td.transaction { - padding-top: 1.5em; -} - -.transaction div { - line-height: 1.25em; - min-height: 1.25em; -} - -.icon { - margin: 0; -} - -.method { - width: 40%; -} - -.transactions td.timestamp { - padding-top: 1.5em; - text-align: right; - line-height: 1.5em; - opacity: 0.5; -} diff --git a/js/src/views/Account/Transactions/transactions.js b/js/src/views/Account/Transactions/transactions.js index 2261284a1..8a205b343 100644 --- a/js/src/views/Account/Transactions/transactions.js +++ b/js/src/views/Account/Transactions/transactions.js @@ -17,12 +17,9 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import LinearProgress from 'material-ui/LinearProgress'; import etherscan from '../../../3rdparty/etherscan'; -import { Container, ContainerTitle } from '../../../ui'; - -import Transaction from './Transaction'; +import { Container, TxList } from '../../../ui'; import styles from './transactions.css'; @@ -33,16 +30,12 @@ class Transactions extends Component { static propTypes = { address: PropTypes.string.isRequired, - accounts: PropTypes.object, - contacts: PropTypes.object, - contracts: PropTypes.object, - tokens: PropTypes.object, isTest: PropTypes.bool, traceMode: PropTypes.bool } state = { - transactions: [], + hashes: [], loading: true, callInfo: {} } @@ -67,38 +60,16 @@ class Transactions extends Component { } render () { - return ( - <Container> - <ContainerTitle title='transactions' /> - { this.renderTransactions() } - </Container> - ); - } - - renderTransactions () { - const { loading, transactions } = this.state; - - if (loading) { - return ( - <LinearProgress mode='indeterminate' /> - ); - } else if (!transactions.length) { - return ( - <div className={ styles.infonone }> - No transactions were found for this account - </div> - ); - } + const { address } = this.props; + const { hashes } = this.state; return ( - <div className={ styles.transactions }> - <table> - <tbody> - { this.renderRows() } - </tbody> - </table> + <Container title='transactions'> + <TxList + address={ address } + hashes={ hashes } /> { this.renderEtherscanFooter() } - </div> + </Container> ); } @@ -116,30 +87,6 @@ class Transactions extends Component { ); } - renderRows () { - const { address, accounts, contacts, contracts, tokens, isTest } = this.props; - const { transactions } = this.state; - - return (transactions || []) - .sort((tA, tB) => { - return tB.blockNumber.comparedTo(tA.blockNumber); - }) - .slice(0, 25) - .map((transaction, index) => { - return ( - <Transaction - key={ index } - transaction={ transaction } - address={ address } - accounts={ accounts } - contacts={ contacts } - contracts={ contracts } - tokens={ tokens } - isTest={ isTest } /> - ); - }); - } - getTransactions = (props) => { const { isTest, address, traceMode } = props; @@ -151,9 +98,9 @@ class Transactions extends Component { return this .fetchTransactions(isTest, address, traceMode) - .then(transactions => { + .then((transactions) => { this.setState({ - transactions, + hashes: transactions.map((transaction) => transaction.hash), loading: false }); }); @@ -204,16 +151,10 @@ class Transactions extends Component { function mapStateToProps (state) { const { isTest, traceMode } = state.nodeStatus; - const { accounts, contacts, contracts } = state.personal; - const { tokens } = state.balances; return { isTest, - traceMode, - accounts, - contacts, - contracts, - tokens + traceMode }; } diff --git a/js/src/views/Contract/contract.js b/js/src/views/Contract/contract.js index fb288f85e..613bf70b9 100644 --- a/js/src/views/Contract/contract.js +++ b/js/src/views/Contract/contract.js @@ -165,7 +165,6 @@ class Contract extends Component { actions={ [ cancelBtn ] } title={ 'contract details' } visible - scroll > <div className={ styles.details }> { this.renderSource(contract) } diff --git a/js/src/views/Dapps/AddDapps/AddDapps.js b/js/src/views/Dapps/AddDapps/AddDapps.js index 8f5df9e91..b18e31704 100644 --- a/js/src/views/Dapps/AddDapps/AddDapps.js +++ b/js/src/views/Dapps/AddDapps/AddDapps.js @@ -50,7 +50,7 @@ export default class AddDapps extends Component { /> ] } visible - scroll> + > <div className={ styles.warning } /> { this.renderList(store.sortedLocal, 'Applications locally available', 'All applications installed locally on the machine by the user for access by the Parity client.') } { this.renderList(store.sortedBuiltin, 'Applications bundled with Parity', 'Experimental applications developed by the Parity team to show off dapp capabilities, integration, experimental features and to control certain network-wide client behaviour.') } diff --git a/js/src/views/Signer/containers/RequestsPage/RequestsPage.js b/js/src/views/Signer/containers/RequestsPage/RequestsPage.js index c53d39264..ae2ba05fb 100644 --- a/js/src/views/Signer/containers/RequestsPage/RequestsPage.js +++ b/js/src/views/Signer/containers/RequestsPage/RequestsPage.js @@ -18,15 +18,17 @@ import BigNumber from 'bignumber.js'; import React, { Component, PropTypes } from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; +import { observer } from 'mobx-react'; import Store from '../../store'; import * as RequestsActions from '../../../../redux/providers/signerActions'; -import { Container, ContainerTitle } from '../../../../ui'; +import { Container, Page, TxList } from '../../../../ui'; import { RequestPending, RequestFinished } from '../../components'; import styles from './RequestsPage.css'; +@observer class RequestsPage extends Component { static contextTypes = { api: PropTypes.object.isRequired @@ -44,20 +46,19 @@ class RequestsPage extends Component { isTest: PropTypes.bool.isRequired }; - store = new Store(this.context.api); + store = new Store(this.context.api, true); + + componentWillUnmount () { + this.store.unsubscribe(); + } render () { - const { pending, finished } = this.props.signer; - - if (!pending.length && !finished.length) { - return this.renderNoRequestsMsg(); - } - return ( - <div> - { this.renderPendingRequests() } - { this.renderFinishedRequests() } - </div> + <Page> + <div>{ this.renderPendingRequests() }</div> + <div>{ this.renderLocalQueue() }</div> + <div>{ this.renderFinishedRequests() }</div> + </Page> ); } @@ -65,18 +66,39 @@ class RequestsPage extends Component { return new BigNumber(b.id).cmp(a.id); } + renderLocalQueue () { + const { localHashes } = this.store; + + if (!localHashes.length) { + return null; + } + + return ( + <Container title='Local Transactions'> + <TxList + address='' + hashes={ localHashes } /> + </Container> + ); + } + renderPendingRequests () { const { pending } = this.props.signer; if (!pending.length) { - return; + return ( + <Container> + <div className={ styles.noRequestsMsg }> + There are no requests requiring your confirmation. + </div> + </Container> + ); } const items = pending.sort(this._sortRequests).map(this.renderPending); return ( - <Container> - <ContainerTitle title='Pending Requests' /> + <Container title='Pending Requests'> <div className={ styles.items }> { items } </div> @@ -94,8 +116,7 @@ class RequestsPage extends Component { const items = finished.sort(this._sortRequests).map(this.renderFinished); return ( - <Container> - <ContainerTitle title='Finished Requests' /> + <Container title='Finished Requests'> <div className={ styles.items }> { items } </div> @@ -143,16 +164,6 @@ class RequestsPage extends Component { /> ); } - - renderNoRequestsMsg () { - return ( - <Container> - <div className={ styles.noRequestsMsg }> - There are no requests requiring your confirmation. - </div> - </Container> - ); - } } function mapStateToProps (state) { diff --git a/js/src/views/Signer/signer.js b/js/src/views/Signer/signer.js index b9aace595..32aca9c82 100644 --- a/js/src/views/Signer/signer.js +++ b/js/src/views/Signer/signer.js @@ -16,7 +16,7 @@ import React, { Component } from 'react'; -import { Actionbar, Page } from '../../ui'; +import { Actionbar } from '../../ui'; import RequestsPage from './containers/RequestsPage'; import styles from './signer.css'; @@ -27,9 +27,7 @@ export default class Signer extends Component { <div className={ styles.signer }> <Actionbar title='Trusted Signer' /> - <Page> - <RequestsPage /> - </Page> + <RequestsPage /> </div> ); } diff --git a/js/src/views/Signer/store.js b/js/src/views/Signer/store.js index 1bb63bbe2..0eeb99861 100644 --- a/js/src/views/Signer/store.js +++ b/js/src/views/Signer/store.js @@ -14,13 +14,26 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. +import { isEqual } from 'lodash'; import { action, observable } from 'mobx'; export default class Store { @observable balances = {}; + @observable localHashes = []; - constructor (api) { + constructor (api, withLocalTransactions = false) { this._api = api; + this._timeoutId = 0; + + if (withLocalTransactions) { + this.fetchLocalTransactions(); + } + } + + @action unsubscribe () { + if (this._timeoutId) { + clearTimeout(this._timeoutId); + } } @action setBalance = (address, balance) => { @@ -31,6 +44,12 @@ export default class Store { this.balances = Object.assign({}, this.balances, balances); } + @action setLocalHashes = (localHashes) => { + if (!isEqual(localHashes, this.localHashes)) { + this.localHashes = localHashes; + } + } + fetchBalance (address) { this._api.eth .getBalance(address) @@ -63,4 +82,18 @@ export default class Store { console.warn('Store:fetchBalances', error); }); } + + fetchLocalTransactions = () => { + const nextTimeout = () => { + this._timeoutId = setTimeout(this.fetchLocalTransactions, 1500); + }; + + this._api.parity + .localTransactions() + .then((localTransactions) => { + this.setLocalHashes(Object.keys(localTransactions)); + }) + .then(nextTimeout) + .catch(nextTimeout); + } } diff --git a/parity/cli/config.invalid3.toml b/parity/cli/config.invalid3.toml new file mode 100644 index 000000000..a43afe03d --- /dev/null +++ b/parity/cli/config.invalid3.toml @@ -0,0 +1,3 @@ +[signer] +passwd = [] + diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index 93373c383..3f67cf1fa 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -649,11 +649,12 @@ mod tests { fn should_parse_config_and_return_errors() { let config1 = Args::parse_config(include_str!("./config.invalid1.toml")); let config2 = Args::parse_config(include_str!("./config.invalid2.toml")); + let config3 = Args::parse_config(include_str!("./config.invalid3.toml")); - match (config1, config2) { - (Err(ArgsError::Parsing(_)), Err(ArgsError::Decode(_))) => {}, - (a, b) => { - assert!(false, "Got invalid error types: {:?}, {:?}", a, b); + match (config1, config2, config3) { + (Err(ArgsError::Parsing(_)), Err(ArgsError::Decode(_)), Err(ArgsError::UnknownFields(_))) => {}, + (a, b, c) => { + assert!(false, "Got invalid error types: {:?}, {:?}, {:?}", a, b, c); } } } diff --git a/parity/cli/usage.rs b/parity/cli/usage.rs index a38c28876..6dcbd6453 100644 --- a/parity/cli/usage.rs +++ b/parity/cli/usage.rs @@ -58,6 +58,7 @@ macro_rules! usage { Parsing(Vec<toml::ParserError>), Decode(toml::DecodeError), Config(String, io::Error), + UnknownFields(String), } impl ArgsError { @@ -80,6 +81,11 @@ macro_rules! usage { println_stderr!("There was an error reading your config file at: {}", path); println_stderr!("{}", e); process::exit(2) + }, + ArgsError::UnknownFields(fields) => { + println_stderr!("You have some extra fields in your config file:"); + println_stderr!("{}", fields); + process::exit(2) } } } @@ -173,10 +179,13 @@ macro_rules! usage { let mut value_parser = toml::Parser::new(&config); match value_parser.parse() { Some(value) => { - let result = rustc_serialize::Decodable::decode(&mut toml::Decoder::new(toml::Value::Table(value))); - match result { - Ok(config) => Ok(config), - Err(e) => Err(e.into()), + let mut decoder = toml::Decoder::new(toml::Value::Table(value)); + let result = rustc_serialize::Decodable::decode(&mut decoder); + + match (result, decoder.toml) { + (Err(e), _) => Err(e.into()), + (_, Some(toml)) => Err(ArgsError::UnknownFields(toml::encode_str(&toml))), + (Ok(config), None) => Ok(config), } }, None => Err(ArgsError::Parsing(value_parser.errors)), diff --git a/rpc/src/v1/types/call_request.rs b/rpc/src/v1/types/call_request.rs index 38721e2f8..015811273 100644 --- a/rpc/src/v1/types/call_request.rs +++ b/rpc/src/v1/types/call_request.rs @@ -19,6 +19,7 @@ use v1::types::{Bytes, H160, U256}; /// Call request #[derive(Debug, Default, PartialEq, Deserialize)] +#[serde(deny_unknown_fields)] pub struct CallRequest { /// From pub from: Option<H160>, diff --git a/rpc/src/v1/types/confirmations.rs b/rpc/src/v1/types/confirmations.rs index 2b7813df9..bbbad83f3 100644 --- a/rpc/src/v1/types/confirmations.rs +++ b/rpc/src/v1/types/confirmations.rs @@ -137,6 +137,7 @@ impl From<helpers::ConfirmationPayload> for ConfirmationPayload { /// Possible modifications to the confirmed transaction sent by `Trusted Signer` #[derive(Debug, PartialEq, Deserialize)] +#[serde(deny_unknown_fields)] pub struct TransactionModification { /// Modified gas price #[serde(rename="gasPrice")] diff --git a/rpc/src/v1/types/histogram.rs b/rpc/src/v1/types/histogram.rs index 385038b56..f7bb5525a 100644 --- a/rpc/src/v1/types/histogram.rs +++ b/rpc/src/v1/types/histogram.rs @@ -21,6 +21,7 @@ use util::stats; /// Values of RPC settings. #[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Histogram { /// Gas prices for bucket edges. #[serde(rename="bucketBounds")] diff --git a/rpc/src/v1/types/rpc_settings.rs b/rpc/src/v1/types/rpc_settings.rs index 9a20afa7a..de8f90410 100644 --- a/rpc/src/v1/types/rpc_settings.rs +++ b/rpc/src/v1/types/rpc_settings.rs @@ -18,6 +18,7 @@ /// Values of RPC settings. #[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct RpcSettings { /// Whether RPC is enabled. pub enabled: bool, @@ -25,4 +26,4 @@ pub struct RpcSettings { pub interface: String, /// The port being listened on. pub port: u64, -} \ No newline at end of file +} diff --git a/rpc/src/v1/types/trace_filter.rs b/rpc/src/v1/types/trace_filter.rs index 21e50e175..6c7460f9b 100644 --- a/rpc/src/v1/types/trace_filter.rs +++ b/rpc/src/v1/types/trace_filter.rs @@ -22,6 +22,7 @@ use v1::types::{BlockNumber, H160}; /// Trace filter #[derive(Debug, PartialEq, Deserialize)] +#[serde(deny_unknown_fields)] pub struct TraceFilter { /// From block #[serde(rename="fromBlock")] diff --git a/rpc/src/v1/types/transaction_request.rs b/rpc/src/v1/types/transaction_request.rs index b7ee1f47d..a4f8e6387 100644 --- a/rpc/src/v1/types/transaction_request.rs +++ b/rpc/src/v1/types/transaction_request.rs @@ -21,6 +21,7 @@ use v1::helpers; /// Transaction request coming from RPC #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct TransactionRequest { /// Sender pub from: H160, diff --git a/sync/src/block_sync.rs b/sync/src/block_sync.rs index ff5411140..eea977906 100644 --- a/sync/src/block_sync.rs +++ b/sync/src/block_sync.rs @@ -258,6 +258,12 @@ impl BlockDownloader { self.blocks.reset_to(hashes); self.state = State::Blocks; return Ok(DownloadAction::Reset); + } else { + let best = io.chain().chain_info().best_block_number; + if best > self.last_imported_block && best - self.last_imported_block > MAX_REORG_BLOCKS { + trace!(target: "sync", "No common block, disabling peer"); + return Err(BlockDownloaderImportError::Invalid); + } } }, State::Blocks => { diff --git a/sync/src/tests/chain.rs b/sync/src/tests/chain.rs index 5fe34428e..7705215f5 100644 --- a/sync/src/tests/chain.rs +++ b/sync/src/tests/chain.rs @@ -250,3 +250,14 @@ fn high_td_attach() { assert_eq!(net.peer(0).chain.chain_info().best_block_number, 5); } + +#[test] +fn disconnect_on_unrelated_chain() { + ::env_logger::init().ok(); + let mut net = TestNet::new(2); + net.peer_mut(0).chain.add_blocks(200, EachBlockWith::Uncle); + net.peer_mut(1).chain.add_blocks(100, EachBlockWith::Nothing); + net.sync(); + assert_eq!(net.disconnect_events, vec![(0, 0)]); +} + diff --git a/sync/src/tests/helpers.rs b/sync/src/tests/helpers.rs index d5e07a936..b1c04f84e 100644 --- a/sync/src/tests/helpers.rs +++ b/sync/src/tests/helpers.rs @@ -123,6 +123,7 @@ pub struct TestPeer { pub struct TestNet { pub peers: Vec<TestPeer>, pub started: bool, + pub disconnect_events: Vec<(PeerId, PeerId)>, //disconnected (initiated by, to) } impl TestNet { @@ -140,6 +141,7 @@ impl TestNet { let mut net = TestNet { peers: Vec::new(), started: false, + disconnect_events: Vec::new(), }; for _ in 0..n { let chain = TestBlockChainClient::new(); @@ -190,6 +192,7 @@ impl TestNet { // notify this that disconnecting peers are disconnecting let mut io = TestIo::new(&mut p.chain, &p.snapshot_service, &mut p.queue, Some(*d)); p.sync.write().on_peer_aborting(&mut io, *d); + self.disconnect_events.push((peer, *d)); } to_disconnect };