From 552a772cc18c10a4926f2fd543f17e038bd0fc98 Mon Sep 17 00:00:00 2001
From: keorn
Date: Thu, 22 Dec 2016 07:06:40 +0100
Subject: [PATCH 01/22] make fields defaulting to 0 optional
---
ethcore/src/spec/genesis.rs | 8 ++++----
ethcore/src/spec/spec.rs | 2 +-
json/src/blockchain/blockchain.rs | 6 +++---
json/src/spec/genesis.rs | 12 ++++++------
json/src/spec/params.rs | 4 ++--
5 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/ethcore/src/spec/genesis.rs b/ethcore/src/spec/genesis.rs
index be3b7c808..1fad0836d 100644
--- a/ethcore/src/spec/genesis.rs
+++ b/ethcore/src/spec/genesis.rs
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-use util::{Address, H256, Uint, U256};
+use util::{Address, H256, Uint, U256, FixedHash};
use util::sha3::SHA3_NULL_RLP;
use ethjson;
use super::seal::Seal;
@@ -50,9 +50,9 @@ impl From for Genesis {
Genesis {
seal: From::from(g.seal),
difficulty: g.difficulty.into(),
- author: g.author.into(),
- timestamp: g.timestamp.into(),
- parent_hash: g.parent_hash.into(),
+ author: g.author.map_or_else(Address::zero, Into::into),
+ timestamp: g.timestamp.map_or(0, Into::into),
+ parent_hash: g.parent_hash.map_or_else(H256::zero, Into::into),
gas_limit: g.gas_limit.into(),
transactions_root: g.transactions_root.map_or_else(|| SHA3_NULL_RLP.clone(), Into::into),
receipts_root: g.receipts_root.map_or_else(|| SHA3_NULL_RLP.clone(), Into::into),
diff --git a/ethcore/src/spec/spec.rs b/ethcore/src/spec/spec.rs
index bdcd5eee2..b6a688402 100644
--- a/ethcore/src/spec/spec.rs
+++ b/ethcore/src/spec/spec.rs
@@ -49,7 +49,7 @@ pub struct CommonParams {
impl From for CommonParams {
fn from(p: ethjson::spec::Params) -> Self {
CommonParams {
- account_start_nonce: p.account_start_nonce.into(),
+ account_start_nonce: p.account_start_nonce.map_or_else(U256::zero, Into::into),
maximum_extra_data_size: p.maximum_extra_data_size.into(),
network_id: p.network_id.into(),
subprotocol_name: p.subprotocol_name.unwrap_or_else(|| "eth".to_owned()),
diff --git a/json/src/blockchain/blockchain.rs b/json/src/blockchain/blockchain.rs
index 8a0de8801..9d18796da 100644
--- a/json/src/blockchain/blockchain.rs
+++ b/json/src/blockchain/blockchain.rs
@@ -59,9 +59,9 @@ impl BlockChain {
mix_hash: self.genesis_block.mix_hash.clone(),
}),
difficulty: self.genesis_block.difficulty,
- author: self.genesis_block.author.clone(),
- timestamp: self.genesis_block.timestamp,
- parent_hash: self.genesis_block.parent_hash.clone(),
+ author: Some(self.genesis_block.author.clone()),
+ timestamp: Some(self.genesis_block.timestamp),
+ parent_hash: Some(self.genesis_block.parent_hash.clone()),
gas_limit: self.genesis_block.gas_limit,
transactions_root: Some(self.genesis_block.transactions_root.clone()),
receipts_root: Some(self.genesis_block.receipts_root.clone()),
diff --git a/json/src/spec/genesis.rs b/json/src/spec/genesis.rs
index c732a1293..393bc49d5 100644
--- a/json/src/spec/genesis.rs
+++ b/json/src/spec/genesis.rs
@@ -28,13 +28,13 @@ pub struct Genesis {
pub seal: Seal,
/// Difficulty.
pub difficulty: Uint,
- /// Block author.
- pub author: Address,
- /// Block timestamp.
- pub timestamp: Uint,
- /// Parent hash.
+ /// Block author, defaults to 0.
+ pub author: Option,
+ /// Block timestamp, defaults to 0.
+ pub timestamp: Option,
+ /// Parent hash, defaults to 0.
#[serde(rename="parentHash")]
- pub parent_hash: H256,
+ pub parent_hash: Option,
/// Gas limit.
#[serde(rename="gasLimit")]
pub gas_limit: Uint,
diff --git a/json/src/spec/params.rs b/json/src/spec/params.rs
index 882686319..f4492f874 100644
--- a/json/src/spec/params.rs
+++ b/json/src/spec/params.rs
@@ -22,9 +22,9 @@ use hash::H256;
/// Spec params.
#[derive(Debug, PartialEq, Deserialize)]
pub struct Params {
- /// Account start nonce.
+ /// Account start nonce, defaults to 0.
#[serde(rename="accountStartNonce")]
- pub account_start_nonce: Uint,
+ pub account_start_nonce: Option,
/// Maximum size of extra data.
#[serde(rename="maximumExtraDataSize")]
pub maximum_extra_data_size: Uint,
From 7c715aeec3449b61d32479898a878613024f3f44 Mon Sep 17 00:00:00 2001
From: Robert Habermeier
Date: Tue, 3 Jan 2017 16:32:50 +0100
Subject: [PATCH 02/22] basic account type
---
ethcore/src/state/account.rs | 67 +++++++++++++++++++++++++++++-------
ethcore/src/state/mod.rs | 2 +-
2 files changed, 55 insertions(+), 14 deletions(-)
diff --git a/ethcore/src/state/account.rs b/ethcore/src/state/account.rs
index 49cebd550..9d6b58d0e 100644
--- a/ethcore/src/state/account.rs
+++ b/ethcore/src/state/account.rs
@@ -25,6 +25,41 @@ use std::cell::{RefCell, Cell};
const STORAGE_CACHE_ITEMS: usize = 8192;
+/// Basic account type.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct BasicAccount {
+ /// Nonce of the account.
+ pub nonce: U256,
+ /// Balance of the account.
+ pub balance: U256,
+ /// Storage root of the account.
+ pub storage_root: H256,
+ /// Code hash of the account.
+ pub code_hash: H256,
+}
+
+impl Encodable for BasicAccount {
+ fn rlp_append(&self, s: &mut RlpStream) {
+ s.begin_list(4)
+ .append(&self.nonce)
+ .append(&self.balance)
+ .append(&self.storage_root)
+ .append(&self.code_hash);
+ }
+}
+
+impl Decodable for BasicAccount {
+ fn decode(decoder: &D) -> Result where D: Decoder {
+ let rlp = decoder.as_rlp();
+ Ok(BasicAccount {
+ nonce: rlp.val_at(0)?,
+ balance: rlp.val_at(1)?,
+ storage_root: rlp.val_at(2)?,
+ code_hash: rlp.val_at(3)?,
+ })
+ }
+}
+
/// Single account in the system.
/// Keeps track of changes to the code and storage.
/// The changes are applied in `commit_storage` and `commit_code`
@@ -53,6 +88,23 @@ pub struct Account {
address_hash: Cell
{ explanation }
+ { this.renderError() }
+ { error.message }
+
+ );
+ }
+
onNameChange = (e) => {
this.setState({ name: e.target.value });
};
@@ -129,9 +147,15 @@ class Reverse extends Component {
this.props.confirm(name);
}
};
+
+ clearError = () => {
+ if (this.props.error) {
+ this.props.clearError();
+ }
+ };
}
const mapStateToProps = (state) => state.reverse;
-const mapDispatchToProps = (dispatch) => bindActionCreators({ propose, confirm }, dispatch);
+const mapDispatchToProps = (dispatch) => bindActionCreators({ clearError, confirm, propose }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Reverse);
diff --git a/js/src/dapps/registry/ui/address.js b/js/src/dapps/registry/ui/address.js
index e3eac2c97..d8e98c220 100644
--- a/js/src/dapps/registry/ui/address.js
+++ b/js/src/dapps/registry/ui/address.js
@@ -20,31 +20,48 @@ import { connect } from 'react-redux';
import Hash from './hash';
import etherscanUrl from '../util/etherscan-url';
import IdentityIcon from '../IdentityIcon';
+import { nullableProptype } from '~/util/proptypes';
import styles from './address.css';
class Address extends Component {
static propTypes = {
address: PropTypes.string.isRequired,
- accounts: PropTypes.object.isRequired,
- contacts: PropTypes.object.isRequired,
+ account: nullableProptype(PropTypes.object.isRequired),
isTestnet: PropTypes.bool.isRequired,
key: PropTypes.string,
shortenHash: PropTypes.bool
- }
+ };
static defaultProps = {
key: 'address',
shortenHash: true
- }
+ };
render () {
- const { address, accounts, contacts, isTestnet, key, shortenHash } = this.props;
+ const { address, key } = this.props;
- let caption;
- if (accounts[address] || contacts[address]) {
- const name = (accounts[address] || contacts[address] || {}).name;
- caption = (
+ return (
+
+
+ { this.renderCaption() }
+
+ );
+ }
+
+ renderCaption () {
+ const { address, account, isTestnet, shortenHash } = this.props;
+
+ if (account) {
+ const { name } = account;
+
+ return (
);
- } else {
- caption = (
-
- { shortenHash ? (
-
- ) : address }
-
- );
}
return (
-
-
- { caption }
-
+
+ { shortenHash ? (
+
+ ) : address }
+
);
}
}
+function mapStateToProps (initState, initProps) {
+ const { accounts, contacts } = initState;
+
+ const allAccounts = Object.assign({}, accounts.all, contacts);
+
+ // Add lower case addresses to map
+ Object
+ .keys(allAccounts)
+ .forEach((address) => {
+ allAccounts[address.toLowerCase()] = allAccounts[address];
+ });
+
+ return (state, props) => {
+ const { isTestnet } = state;
+ const { address = '' } = props;
+
+ const account = allAccounts[address] || null;
+
+ return {
+ account,
+ isTestnet
+ };
+ };
+}
+
export default connect(
- // mapStateToProps
- (state) => ({
- accounts: state.accounts.all,
- contacts: state.contacts,
- isTestnet: state.isTestnet
- }),
- // mapDispatchToProps
- null
+ mapStateToProps
)(Address);
diff --git a/js/src/dapps/registry/ui/image.js b/js/src/dapps/registry/ui/image.js
index c66e34128..c7774bfac 100644
--- a/js/src/dapps/registry/ui/image.js
+++ b/js/src/dapps/registry/ui/image.js
@@ -23,10 +23,20 @@ const styles = {
border: '1px solid #777'
};
-export default (address) => (
-
-);
+export default (address) => {
+ if (!address || /^(0x)?0*$/.test(address)) {
+ return (
+
+ No image
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/js/src/dapps/registry/util/actions.js b/js/src/dapps/registry/util/actions.js
index 0f4f350fc..1ae7426de 100644
--- a/js/src/dapps/registry/util/actions.js
+++ b/js/src/dapps/registry/util/actions.js
@@ -19,7 +19,7 @@ export const isAction = (ns, type, action) => {
};
export const isStage = (stage, action) => {
- return action.type.slice(-1 - stage.length) === ` ${stage}`;
+ return (new RegExp(`${stage}$`)).test(action.type);
};
export const addToQueue = (queue, action, name) => {
@@ -27,5 +27,5 @@ export const addToQueue = (queue, action, name) => {
};
export const removeFromQueue = (queue, action, name) => {
- return queue.filter((e) => e.action === action && e.name === name);
+ return queue.filter((e) => !(e.action === action && e.name === name));
};
diff --git a/js/src/dapps/registry/util/post-tx.js b/js/src/dapps/registry/util/post-tx.js
index 84326dcab..298bbd843 100644
--- a/js/src/dapps/registry/util/post-tx.js
+++ b/js/src/dapps/registry/util/post-tx.js
@@ -24,12 +24,6 @@ const postTx = (api, method, opt = {}, values = []) => {
})
.then((reqId) => {
return api.pollMethod('parity_checkRequest', reqId);
- })
- .catch((err) => {
- if (err && err.type === 'REQUEST_REJECTED') {
- throw new Error('The request has been rejected.');
- }
- throw err;
});
};
diff --git a/js/src/dapps/registry/util/registry.js b/js/src/dapps/registry/util/registry.js
new file mode 100644
index 000000000..371b29aec
--- /dev/null
+++ b/js/src/dapps/registry/util/registry.js
@@ -0,0 +1,37 @@
+// Copyright 2015, 2016 Parity Technologies (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 const getOwner = (contract, name) => {
+ const { address, api } = contract;
+
+ const key = api.util.sha3(name) + '0000000000000000000000000000000000000000000000000000000000000001';
+ const position = api.util.sha3(key, { encoding: 'hex' });
+
+ return api
+ .eth
+ .getStorageAt(address, position)
+ .then((result) => {
+ if (/^(0x)?0*$/.test(result)) {
+ return '';
+ }
+
+ return '0x' + result.slice(-40);
+ });
+};
+
+export const isOwned = (contract, name) => {
+ return getOwner(contract, name).then((owner) => !!owner);
+};
diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js
index 02b7ef266..99bd1c5f3 100644
--- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js
+++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js
@@ -20,6 +20,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import RaisedButton from 'material-ui/RaisedButton';
import ReactTooltip from 'react-tooltip';
+import keycode from 'keycode';
import { Form, Input, IdentityIcon } from '~/ui';
@@ -207,7 +208,9 @@ class TransactionPendingFormConfirm extends Component {
}
onKeyDown = (event) => {
- if (event.which !== 13) {
+ const codeName = keycode(event);
+
+ if (codeName !== 'enter') {
return;
}
diff --git a/js/src/views/Wallet/wallet.js b/js/src/views/Wallet/wallet.js
index 5fe6c957e..5418448b4 100644
--- a/js/src/views/Wallet/wallet.js
+++ b/js/src/views/Wallet/wallet.js
@@ -71,7 +71,7 @@ class Wallet extends Component {
owned: PropTypes.bool.isRequired,
setVisibleAccounts: PropTypes.func.isRequired,
wallet: PropTypes.object.isRequired,
- walletAccount: nullableProptype(PropTypes.object).isRequired
+ walletAccount: nullableProptype(PropTypes.object.isRequired)
};
state = {
From 7cdfaf1a439a5287b55b4195b29878193f0d7049 Mon Sep 17 00:00:00 2001
From: Jaco Greeff
Date: Wed, 4 Jan 2017 15:14:51 +0100
Subject: [PATCH 10/22] Unsubscribe error on ShapeShift modal close (#4005)
* unsubscribe in onClose (state available)
* Revert "unsubscribe in onClose (state available)"
This reverts commit 1da0a7447563e3cb0d9149b0b9898ec93b483982.
* Fix shapeshift double unsubscribe
* Swap multiple list test addresses
---
js/src/3rdparty/shapeshift/helpers.spec.js | 18 +--
js/src/3rdparty/shapeshift/rpc.spec.js | 19 ++-
js/src/3rdparty/shapeshift/shapeshift.js | 77 +++++++-----
js/src/3rdparty/shapeshift/shapeshift.spec.js | 110 ++++++++++++++++--
4 files changed, 168 insertions(+), 56 deletions(-)
diff --git a/js/src/3rdparty/shapeshift/helpers.spec.js b/js/src/3rdparty/shapeshift/helpers.spec.js
index 8ccec6791..d5e9994b8 100644
--- a/js/src/3rdparty/shapeshift/helpers.spec.js
+++ b/js/src/3rdparty/shapeshift/helpers.spec.js
@@ -16,16 +16,10 @@
const nock = require('nock');
-const ShapeShift = require('./');
-const initShapeshift = (ShapeShift.default || ShapeShift);
-
const APIKEY = '0x123454321';
-const shapeshift = initShapeshift(APIKEY);
-const rpc = shapeshift.getRpc();
-
-function mockget (requests) {
- let scope = nock(rpc.ENDPOINT);
+function mockget (shapeshift, requests) {
+ let scope = nock(shapeshift.getRpc().ENDPOINT);
requests.forEach((request) => {
scope = scope
@@ -38,8 +32,8 @@ function mockget (requests) {
return scope;
}
-function mockpost (requests) {
- let scope = nock(rpc.ENDPOINT);
+function mockpost (shapeshift, requests) {
+ let scope = nock(shapeshift.getRpc().ENDPOINT);
requests.forEach((request) => {
scope = scope
@@ -58,7 +52,5 @@ function mockpost (requests) {
module.exports = {
APIKEY,
mockget,
- mockpost,
- shapeshift,
- rpc
+ mockpost
};
diff --git a/js/src/3rdparty/shapeshift/rpc.spec.js b/js/src/3rdparty/shapeshift/rpc.spec.js
index 582b77a53..d561fbe7d 100644
--- a/js/src/3rdparty/shapeshift/rpc.spec.js
+++ b/js/src/3rdparty/shapeshift/rpc.spec.js
@@ -16,12 +16,21 @@
const helpers = require('./helpers.spec.js');
-const APIKEY = helpers.APIKEY;
+const ShapeShift = require('./');
+const initShapeshift = (ShapeShift.default || ShapeShift);
+
const mockget = helpers.mockget;
const mockpost = helpers.mockpost;
-const rpc = helpers.rpc;
describe('shapeshift/rpc', () => {
+ let rpc;
+ let shapeshift;
+
+ beforeEach(() => {
+ shapeshift = initShapeshift(helpers.APIKEY);
+ rpc = shapeshift.getRpc();
+ });
+
describe('GET', () => {
const REPLY = { test: 'this is some result' };
@@ -29,7 +38,7 @@ describe('shapeshift/rpc', () => {
let result;
beforeEach(() => {
- scope = mockget([{ path: 'test', reply: REPLY }]);
+ scope = mockget(shapeshift, [{ path: 'test', reply: REPLY }]);
return rpc
.get('test')
@@ -54,7 +63,7 @@ describe('shapeshift/rpc', () => {
let result;
beforeEach(() => {
- scope = mockpost([{ path: 'test', reply: REPLY }]);
+ scope = mockpost(shapeshift, [{ path: 'test', reply: REPLY }]);
return rpc
.post('test', { input: 'stuff' })
@@ -76,7 +85,7 @@ describe('shapeshift/rpc', () => {
});
it('passes the apikey specified', () => {
- expect(scope.body.test.apiKey).to.equal(APIKEY);
+ expect(scope.body.test.apiKey).to.equal(helpers.APIKEY);
});
});
});
diff --git a/js/src/3rdparty/shapeshift/shapeshift.js b/js/src/3rdparty/shapeshift/shapeshift.js
index 39b8365ec..c98ef3eca 100644
--- a/js/src/3rdparty/shapeshift/shapeshift.js
+++ b/js/src/3rdparty/shapeshift/shapeshift.js
@@ -15,8 +15,9 @@
// along with Parity. If not, see .
export default function (rpc) {
- let subscriptions = [];
- let pollStatusIntervalId = null;
+ let _subscriptions = [];
+ let _pollStatusIntervalId = null;
+ let _subscriptionPromises = null;
function getCoins () {
return rpc.get('getcoins');
@@ -36,75 +37,93 @@ export default function (rpc) {
function shift (toAddress, returnAddress, pair) {
return rpc.post('shift', {
- withdrawal: toAddress,
- pair: pair,
- returnAddress: returnAddress
+ pair,
+ returnAddress,
+ withdrawal: toAddress
});
}
function subscribe (depositAddress, callback) {
- const idx = subscriptions.length;
+ if (!depositAddress || !callback) {
+ return;
+ }
- subscriptions.push({
- depositAddress,
+ const index = _subscriptions.length;
+
+ _subscriptions.push({
callback,
- idx
+ depositAddress,
+ index
});
- // Only poll if there are subscriptions...
- if (!pollStatusIntervalId) {
- pollStatusIntervalId = setInterval(_pollStatus, 2000);
+ if (_pollStatusIntervalId === null) {
+ _pollStatusIntervalId = setInterval(_pollStatus, 2000);
}
}
function unsubscribe (depositAddress) {
- const newSubscriptions = []
- .concat(subscriptions)
- .filter((sub) => sub.depositAddress !== depositAddress);
+ _subscriptions = _subscriptions.filter((sub) => sub.depositAddress !== depositAddress);
- subscriptions = newSubscriptions;
-
- if (subscriptions.length === 0) {
- clearInterval(pollStatusIntervalId);
- pollStatusIntervalId = null;
+ if (_subscriptions.length === 0) {
+ clearInterval(_pollStatusIntervalId);
+ _pollStatusIntervalId = null;
}
+
+ return true;
}
function _getSubscriptionStatus (subscription) {
if (!subscription) {
- return;
+ return Promise.resolve();
}
- getStatus(subscription.depositAddress)
+ return getStatus(subscription.depositAddress)
.then((result) => {
switch (result.status) {
case 'no_deposits':
case 'received':
subscription.callback(null, result);
- return;
+ return true;
case 'complete':
subscription.callback(null, result);
- subscriptions[subscription.idx] = null;
- return;
+ unsubscribe(subscription.depositAddress);
+ return true;
case 'failed':
subscription.callback({
message: status.error,
fatal: true
});
- subscriptions[subscription.idx] = null;
- return;
+ unsubscribe(subscription.depositAddress);
+ return true;
}
})
- .catch(subscription.callback);
+ .catch(() => {
+ return true;
+ });
}
function _pollStatus () {
- subscriptions.forEach(_getSubscriptionStatus);
+ _subscriptionPromises = Promise.all(_subscriptions.map(_getSubscriptionStatus));
+ }
+
+ function _getSubscriptions () {
+ return _subscriptions;
+ }
+
+ function _getSubscriptionPromises () {
+ return _subscriptionPromises;
+ }
+
+ function _isPolling () {
+ return _pollStatusIntervalId !== null;
}
return {
+ _getSubscriptions,
+ _getSubscriptionPromises,
+ _isPolling,
getCoins,
getMarketInfo,
getRpc,
diff --git a/js/src/3rdparty/shapeshift/shapeshift.spec.js b/js/src/3rdparty/shapeshift/shapeshift.spec.js
index 9fe2ca1f0..1897178b3 100644
--- a/js/src/3rdparty/shapeshift/shapeshift.spec.js
+++ b/js/src/3rdparty/shapeshift/shapeshift.spec.js
@@ -14,13 +14,29 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
+const sinon = require('sinon');
+
+const ShapeShift = require('./');
+const initShapeshift = (ShapeShift.default || ShapeShift);
+
const helpers = require('./helpers.spec.js');
const mockget = helpers.mockget;
const mockpost = helpers.mockpost;
-const shapeshift = helpers.shapeshift;
describe('shapeshift/calls', () => {
+ let clock;
+ let shapeshift;
+
+ beforeEach(() => {
+ clock = sinon.useFakeTimers();
+ shapeshift = initShapeshift(helpers.APIKEY);
+ });
+
+ afterEach(() => {
+ clock.restore();
+ });
+
describe('getCoins', () => {
const REPLY = {
BTC: {
@@ -39,8 +55,8 @@ describe('shapeshift/calls', () => {
let scope;
- before(() => {
- scope = mockget([{ path: 'getcoins', reply: REPLY }]);
+ beforeEach(() => {
+ scope = mockget(shapeshift, [{ path: 'getcoins', reply: REPLY }]);
return shapeshift.getCoins();
});
@@ -61,8 +77,8 @@ describe('shapeshift/calls', () => {
let scope;
- before(() => {
- scope = mockget([{ path: 'marketinfo/btc_ltc', reply: REPLY }]);
+ beforeEach(() => {
+ scope = mockget(shapeshift, [{ path: 'marketinfo/btc_ltc', reply: REPLY }]);
return shapeshift.getMarketInfo('btc_ltc');
});
@@ -80,8 +96,8 @@ describe('shapeshift/calls', () => {
let scope;
- before(() => {
- scope = mockget([{ path: 'txStat/0x123', reply: REPLY }]);
+ beforeEach(() => {
+ scope = mockget(shapeshift, [{ path: 'txStat/0x123', reply: REPLY }]);
return shapeshift.getStatus('0x123');
});
@@ -101,8 +117,8 @@ describe('shapeshift/calls', () => {
let scope;
- before(() => {
- scope = mockpost([{ path: 'shift', reply: REPLY }]);
+ beforeEach(() => {
+ scope = mockpost(shapeshift, [{ path: 'shift', reply: REPLY }]);
return shapeshift.shift('0x456', '1BTC', 'btc_eth');
});
@@ -125,4 +141,80 @@ describe('shapeshift/calls', () => {
});
});
});
+
+ describe('subscriptions', () => {
+ const ADDRESS = '0123456789abcdef';
+ const REPLY = {
+ status: 'complete',
+ address: ADDRESS
+ };
+
+ let callback;
+
+ beforeEach(() => {
+ mockget(shapeshift, [{ path: `txStat/${ADDRESS}`, reply: REPLY }]);
+ callback = sinon.stub();
+ shapeshift.subscribe(ADDRESS, callback);
+ });
+
+ describe('subscribe', () => {
+ it('adds the depositAddress to the list', () => {
+ const subscriptions = shapeshift._getSubscriptions();
+
+ expect(subscriptions.length).to.equal(1);
+ expect(subscriptions[0].depositAddress).to.equal(ADDRESS);
+ });
+
+ it('starts the polling timer', () => {
+ expect(shapeshift._isPolling()).to.be.true;
+ });
+
+ it('calls the callback once the timer has elapsed', () => {
+ clock.tick(2222);
+
+ return shapeshift._getSubscriptionPromises().then(() => {
+ expect(callback).to.have.been.calledWith(null, REPLY);
+ });
+ });
+
+ it('auto-unsubscribes on completed', () => {
+ clock.tick(2222);
+
+ return shapeshift._getSubscriptionPromises().then(() => {
+ expect(shapeshift._getSubscriptions().length).to.equal(0);
+ });
+ });
+ });
+
+ describe('unsubscribe', () => {
+ it('unbsubscribes when requested', () => {
+ expect(shapeshift._getSubscriptions().length).to.equal(1);
+ shapeshift.unsubscribe(ADDRESS);
+ expect(shapeshift._getSubscriptions().length).to.equal(0);
+ });
+
+ it('clears the polling on no subscriptions', () => {
+ shapeshift.unsubscribe(ADDRESS);
+ expect(shapeshift._isPolling()).to.be.false;
+ });
+
+ it('handles unsubscribe of auto-unsubscribe', () => {
+ clock.tick(2222);
+
+ return shapeshift._getSubscriptionPromises().then(() => {
+ expect(shapeshift.unsubscribe(ADDRESS)).to.be.true;
+ });
+ });
+
+ it('handles unsubscribe when multiples listed', () => {
+ const ADDRESS2 = 'abcdef0123456789';
+
+ shapeshift.subscribe(ADDRESS2, sinon.stub());
+ expect(shapeshift._getSubscriptions().length).to.equal(2);
+ expect(shapeshift._getSubscriptions()[0].depositAddress).to.equal(ADDRESS);
+ shapeshift.unsubscribe(ADDRESS);
+ expect(shapeshift._getSubscriptions()[0].depositAddress).to.equal(ADDRESS2);
+ });
+ });
+ });
});
From cc8e200ed50bc5b3f56d57d647b7182123aa70e2 Mon Sep 17 00:00:00 2001
From: Jaco Greeff
Date: Wed, 4 Jan 2017 15:15:11 +0100
Subject: [PATCH 11/22] Connection UI cleanups & tests for prior PR (#4020)
* Cleanups & tests for #3945
* Externalise icons as per PR comments
---
js/src/ui/Icons/index.js | 10 +-
js/src/views/Connection/connection.js | 54 +++----
js/src/views/Connection/connection.spec.js | 156 +++++++++++++++++++++
3 files changed, 195 insertions(+), 25 deletions(-)
create mode 100644 js/src/views/Connection/connection.spec.js
diff --git a/js/src/ui/Icons/index.js b/js/src/ui/Icons/index.js
index 8901d7dc7..4cf5a2d7d 100644
--- a/js/src/ui/Icons/index.js
+++ b/js/src/ui/Icons/index.js
@@ -18,7 +18,10 @@ import AddIcon from 'material-ui/svg-icons/content/add';
import CancelIcon from 'material-ui/svg-icons/content/clear';
import CheckIcon from 'material-ui/svg-icons/navigation/check';
import CloseIcon from 'material-ui/svg-icons/navigation/close';
+import CompareIcon from 'material-ui/svg-icons/action/compare-arrows';
+import ComputerIcon from 'material-ui/svg-icons/hardware/desktop-mac';
import ContractIcon from 'material-ui/svg-icons/action/code';
+import DashboardIcon from 'material-ui/svg-icons/action/dashboard';
import DoneIcon from 'material-ui/svg-icons/action/done-all';
import LockedIcon from 'material-ui/svg-icons/action/lock-outline';
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
@@ -27,13 +30,17 @@ import SaveIcon from 'material-ui/svg-icons/content/save';
import SendIcon from 'material-ui/svg-icons/content/send';
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
+import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';
export {
AddIcon,
CancelIcon,
CheckIcon,
CloseIcon,
+ CompareIcon,
+ ComputerIcon,
ContractIcon,
+ DashboardIcon,
DoneIcon,
LockedIcon,
NextIcon,
@@ -41,5 +48,6 @@ export {
SaveIcon,
SendIcon,
SnoozeIcon,
- VisibleIcon
+ VisibleIcon,
+ VpnIcon
};
diff --git a/js/src/views/Connection/connection.js b/js/src/views/Connection/connection.js
index 4f2ae7b1d..505840e1e 100644
--- a/js/src/views/Connection/connection.js
+++ b/js/src/views/Connection/connection.js
@@ -17,13 +17,9 @@
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
-import ActionCompareArrows from 'material-ui/svg-icons/action/compare-arrows';
-import ActionDashboard from 'material-ui/svg-icons/action/dashboard';
-import HardwareDesktopMac from 'material-ui/svg-icons/hardware/desktop-mac';
-import NotificationVpnLock from 'material-ui/svg-icons/notification/vpn-lock';
import { Input } from '~/ui';
+import { CompareIcon, ComputerIcon, DashboardIcon, VpnIcon } from '~/ui/Icons';
import styles from './connection.css';
@@ -51,13 +47,6 @@ class Connection extends Component {
return null;
}
- const typeIcon = needsToken
- ?
- : ;
- const description = needsToken
- ? this.renderSigner()
- : this.renderPing();
-
return (
@@ -65,16 +54,24 @@ class Connection extends Component {
-
+
- { typeIcon }
+ {
+ needsToken
+ ?
+ :
+ }
- { description }
+ {
+ needsToken
+ ? this.renderSigner()
+ : this.renderPing()
+ }
@@ -144,10 +141,19 @@ class Connection extends Component {
);
}
- onChangeToken = (event, _token) => {
+ validateToken = (_token) => {
const token = _token.trim();
const validToken = /^[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}$/.test(token);
+ return {
+ token,
+ validToken
+ };
+ }
+
+ onChangeToken = (event, _token) => {
+ const { token, validToken } = this.validateToken(_token || event.target.value);
+
this.setState({ token, validToken }, () => {
validToken && this.setToken();
});
@@ -159,7 +165,7 @@ class Connection extends Component {
this.setState({ loading: true });
- api
+ return api
.updateToken(token, 0)
.then((isValid) => {
this.setState({
@@ -173,14 +179,14 @@ class Connection extends Component {
function mapStateToProps (state) {
const { isConnected, isConnecting, needsToken } = state.nodeStatus;
- return { isConnected, isConnecting, needsToken };
-}
-
-function mapDispatchToProps (dispatch) {
- return bindActionCreators({}, dispatch);
+ return {
+ isConnected,
+ isConnecting,
+ needsToken
+ };
}
export default connect(
mapStateToProps,
- mapDispatchToProps
+ null
)(Connection);
diff --git a/js/src/views/Connection/connection.spec.js b/js/src/views/Connection/connection.spec.js
new file mode 100644
index 000000000..20c41b3e4
--- /dev/null
+++ b/js/src/views/Connection/connection.spec.js
@@ -0,0 +1,156 @@
+// Copyright 2015, 2016 Parity Technologies (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 { shallow } from 'enzyme';
+import React from 'react';
+import sinon from 'sinon';
+
+import Connection from './';
+
+let api;
+let component;
+let instance;
+
+function createApi () {
+ return {
+ updateToken: sinon.stub().resolves()
+ };
+}
+
+function createRedux (isConnected = true, isConnecting = false, needsToken = false) {
+ return {
+ dispatch: sinon.stub(),
+ subscribe: sinon.stub(),
+ getState: () => {
+ return {
+ nodeStatus: {
+ isConnected,
+ isConnecting,
+ needsToken
+ }
+ };
+ }
+ };
+}
+
+function render (store) {
+ api = createApi();
+ component = shallow(
+ ,
+ { context: { store: store || createRedux() } }
+ ).find('Connection').shallow({ context: { api } });
+ instance = component.instance();
+
+ return component;
+}
+
+describe('views/Connection', () => {
+ it('renders defaults', () => {
+ expect(render()).to.be.ok;
+ });
+
+ it('does not render when connected', () => {
+ expect(render(createRedux(true)).find('div')).to.have.length(0);
+ });
+
+ describe('renderPing', () => {
+ it('renders the connecting to node message', () => {
+ render();
+ const ping = shallow(instance.renderPing());
+
+ expect(ping.find('FormattedMessage').props().id).to.equal('connection.connectingNode');
+ });
+ });
+
+ describe('renderSigner', () => {
+ it('renders the connecting to api message when isConnecting === true', () => {
+ render(createRedux(false, true));
+ const signer = shallow(instance.renderSigner());
+
+ expect(signer.find('FormattedMessage').props().id).to.equal('connection.connectingAPI');
+ });
+
+ it('renders token input when needsToken == true & isConnecting === false', () => {
+ render(createRedux(false, false, true));
+ const signer = shallow(instance.renderSigner());
+
+ expect(signer.find('FormattedMessage').first().props().id).to.equal('connection.noConnection');
+ });
+ });
+
+ describe('validateToken', () => {
+ beforeEach(() => {
+ render();
+ });
+
+ it('trims whitespace from passed tokens', () => {
+ expect(instance.validateToken(' \t test ing\t ').token).to.equal('test ing');
+ });
+
+ it('validates 4-4-4-4 format', () => {
+ expect(instance.validateToken('1234-5678-90ab-cdef').validToken).to.be.true;
+ });
+
+ it('validates 4-4-4-4 format (with trimmable whitespace)', () => {
+ expect(instance.validateToken(' \t 1234-5678-90ab-cdef \t ').validToken).to.be.true;
+ });
+
+ it('validates 4444 format', () => {
+ expect(instance.validateToken('1234567890abcdef').validToken).to.be.true;
+ });
+
+ it('validates 4444 format (with trimmable whitespace)', () => {
+ expect(instance.validateToken(' \t 1234567890abcdef \t ').validToken).to.be.true;
+ });
+ });
+
+ describe('onChangeToken', () => {
+ beforeEach(() => {
+ render();
+ sinon.spy(instance, 'setToken');
+ sinon.spy(instance, 'validateToken');
+ });
+
+ afterEach(() => {
+ instance.setToken.restore();
+ instance.validateToken.restore();
+ });
+
+ it('validates tokens passed', () => {
+ instance.onChangeToken({ target: { value: 'testing' } });
+ expect(instance.validateToken).to.have.been.calledWith('testing');
+ });
+
+ it('sets the token on the api when valid', () => {
+ instance.onChangeToken({ target: { value: '1234-5678-90ab-cdef' } });
+ expect(instance.setToken).to.have.been.called;
+ });
+ });
+
+ describe('setToken', () => {
+ beforeEach(() => {
+ render();
+ });
+
+ it('calls the api.updateToken', () => {
+ component.setState({ token: 'testing' });
+
+ return instance.setToken().then(() => {
+ expect(api.updateToken).to.have.been.calledWith('testing');
+ });
+ });
+ });
+});
From 71e7a429d76948dedb539b52e9ffda9c023829cb Mon Sep 17 00:00:00 2001
From: Nicolas Gotchac
Date: Wed, 4 Jan 2017 15:15:25 +0100
Subject: [PATCH 12/22] Only fetch App when necessary (#4023)
* Only fetch App when necessary. Show loadings + 404 #3914
* PR Grumble
---
js/src/views/Dapp/dapp.css | 20 +++++++++++
js/src/views/Dapp/dapp.js | 59 +++++++++++++++++++++++++++++---
js/src/views/Dapps/dapps.js | 4 +++
js/src/views/Dapps/dappsStore.js | 36 +++++++++++++++----
4 files changed, 109 insertions(+), 10 deletions(-)
diff --git a/js/src/views/Dapp/dapp.css b/js/src/views/Dapp/dapp.css
index b0f8f06e6..70b39ce08 100644
--- a/js/src/views/Dapp/dapp.css
+++ b/js/src/views/Dapp/dapp.css
@@ -20,3 +20,23 @@
height: 100%;
width: 100%;
}
+
+.full {
+ width: 100vw;
+ height: 100vh;
+ margin: 0;
+ padding: 0;
+ background: white;
+ font-family: 'Roboto', sans-serif;
+ font-size: 16px;
+ font-weight: 300;
+
+ .text {
+ text-align: center;
+ padding: 5em;
+ font-size: 2em;
+ color: #999;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
diff --git a/js/src/views/Dapp/dapp.js b/js/src/views/Dapp/dapp.js
index 87245ca72..7f1416392 100644
--- a/js/src/views/Dapp/dapp.js
+++ b/js/src/views/Dapp/dapp.js
@@ -16,6 +16,7 @@
import React, { Component, PropTypes } from 'react';
import { observer } from 'mobx-react';
+import { FormattedMessage } from 'react-intl';
import DappsStore from '../Dapps/dappsStore';
@@ -25,21 +26,71 @@ import styles from './dapp.css';
export default class Dapp extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
- }
+ };
static propTypes = {
params: PropTypes.object
};
+ state = {
+ app: null,
+ loading: true
+ };
+
store = DappsStore.get(this.context.api);
+ componentWillMount () {
+ const { id } = this.props.params;
+ this.loadApp(id);
+ }
+
+ componentWillReceiveProps (nextProps) {
+ if (nextProps.params.id !== this.props.params.id) {
+ this.loadApp(nextProps.params.id);
+ }
+ }
+
+ loadApp (id) {
+ this.setState({ loading: true });
+
+ this.store
+ .loadApp(id)
+ .then((app) => {
+ this.setState({ loading: false, app });
+ })
+ .catch(() => {
+ this.setState({ loading: false });
+ });
+ }
+
render () {
const { dappsUrl } = this.context.api;
- const { id } = this.props.params;
- const app = this.store.apps.find((app) => app.id === id);
+ const { app, loading } = this.state;
+
+ if (loading) {
+ return (
+
+ );
+ }
if (!app) {
- return null;
+ return (
+
+ );
}
let src = null;
diff --git a/js/src/views/Dapps/dapps.js b/js/src/views/Dapps/dapps.js
index e800263e4..fa7c44878 100644
--- a/js/src/views/Dapps/dapps.js
+++ b/js/src/views/Dapps/dapps.js
@@ -45,6 +45,10 @@ class Dapps extends Component {
store = DappsStore.get(this.context.api);
permissionStore = new PermissionStore(this.context.api);
+ componentWillMount () {
+ this.store.loadAllApps();
+ }
+
render () {
let externalOverlay = null;
if (this.store.externalOverlayVisible) {
diff --git a/js/src/views/Dapps/dappsStore.js b/js/src/views/Dapps/dappsStore.js
index 42342aff3..8cca4d3f7 100644
--- a/js/src/views/Dapps/dappsStore.js
+++ b/js/src/views/Dapps/dappsStore.js
@@ -48,17 +48,43 @@ export default class DappsStore {
this.readDisplayApps();
this.loadExternalOverlay();
- this.loadApps();
this.subscribeToChanges();
}
- loadApps () {
+ /**
+ * Try to find the app from the local (local or builtin)
+ * apps, else fetch from the node
+ */
+ loadApp (id) {
const { dappReg } = Contracts.get();
- Promise
+ return this
+ .loadLocalApps()
+ .then(() => {
+ const app = this.apps.find((app) => app.id === id);
+
+ if (app) {
+ return app;
+ }
+
+ return this.fetchRegistryApp(dappReg, id, true);
+ });
+ }
+
+ loadLocalApps () {
+ return Promise
.all([
this.fetchBuiltinApps().then((apps) => this.addApps(apps)),
- this.fetchLocalApps().then((apps) => this.addApps(apps)),
+ this.fetchLocalApps().then((apps) => this.addApps(apps))
+ ]);
+ }
+
+ loadAllApps () {
+ const { dappReg } = Contracts.get();
+
+ return Promise
+ .all([
+ this.loadLocalApps(),
this.fetchRegistryApps(dappReg).then((apps) => this.addApps(apps))
])
.then(this.writeDisplayApps);
@@ -67,8 +93,6 @@ export default class DappsStore {
static get (api) {
if (!instance) {
instance = new DappsStore(api);
- } else {
- instance.loadApps();
}
return instance;
From 839ee9afd7a4ee95117eddb4470fc9ec4bc9f9a6 Mon Sep 17 00:00:00 2001
From: Jannis Redmann
Date: Wed, 4 Jan 2017 15:15:36 +0100
Subject: [PATCH 13/22] address selector: support reverse lookup (#4033)
* address selector: simplify lookup
* address selector: support reverse lookup
---
js/src/ui/Form/AddressSelect/addressSelect.js | 3 +-
.../Form/AddressSelect/addressSelectStore.js | 128 +++++++++++-------
2 files changed, 83 insertions(+), 48 deletions(-)
diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js
index ca4e1f524..ba48b6489 100644
--- a/js/src/ui/Form/AddressSelect/addressSelect.js
+++ b/js/src/ui/Form/AddressSelect/addressSelect.js
@@ -215,8 +215,9 @@ class AddressSelect extends Component {
}
const { address, addressError } = validateAddress(inputValue);
+ const { registryValues } = this.store;
- if (addressError) {
+ if (addressError || registryValues.length > 0) {
return null;
}
diff --git a/js/src/ui/Form/AddressSelect/addressSelectStore.js b/js/src/ui/Form/AddressSelect/addressSelectStore.js
index b6827b9cc..26f9fe80e 100644
--- a/js/src/ui/Form/AddressSelect/addressSelectStore.js
+++ b/js/src/ui/Form/AddressSelect/addressSelectStore.js
@@ -22,6 +22,8 @@ import { FormattedMessage } from 'react-intl';
import Contracts from '~/contracts';
import { sha3 } from '~/api/util/sha3';
+const ZERO = /^(0x)?0*$/;
+
export default class AddressSelectStore {
@observable values = [];
@@ -38,41 +40,75 @@ export default class AddressSelectStore {
registry
.getContract('emailverification')
.then((emailVerification) => {
- this.regLookups.push({
- lookup: (value) => {
- return emailVerification
- .instance
- .reverse.call({}, [ sha3(value) ]);
- },
- describe: (value) => (
-
- )
+ this.regLookups.push((email) => {
+ return emailVerification
+ .instance
+ .reverse
+ .call({}, [ sha3(email) ])
+ .then((address) => {
+ return {
+ address,
+ description: (
+
+ )
+ };
+ });
});
});
registry
.getInstance()
.then((registryInstance) => {
- this.regLookups.push({
- lookup: (value) => {
- return registryInstance
- .getAddress.call({}, [ sha3(value), 'A' ]);
- },
- describe: (value) => (
-
- )
+ this.regLookups.push((name) => {
+ return registryInstance
+ .getAddress
+ .call({}, [ sha3(name), 'A' ])
+ .then((address) => {
+ return {
+ address,
+ name,
+ description: (
+
+ )
+ };
+ });
+ });
+
+ this.regLookups.push((address) => {
+ return registryInstance
+ .reverse
+ .call({}, [ address ])
+ .then((name) => {
+ if (!name) {
+ return null;
+ }
+
+ return {
+ address,
+ name,
+ description: (
+
+ )
+ };
+ });
});
});
}
@@ -149,32 +185,30 @@ export default class AddressSelectStore {
// Registries Lookup
this.registryValues = [];
- const lookups = this.regLookups.map((regLookup) => regLookup.lookup(value));
+ const lookups = this.regLookups.map((regLookup) => regLookup(value));
Promise
.all(lookups)
.then((results) => {
return results
- .map((result, index) => {
- if (/^(0x)?0*$/.test(result)) {
- return;
- }
-
- const lowercaseResult = result.toLowerCase();
+ .filter((result) => result && !ZERO.test(result.address));
+ })
+ .then((results) => {
+ this.registryValues = results
+ .map((result) => {
+ const lowercaseAddress = result.address.toLowerCase();
const account = flatMap(this.initValues, (cat) => cat.values)
- .find((account) => account.address.toLowerCase() === lowercaseResult);
+ .find((account) => account.address.toLowerCase() === lowercaseAddress);
- return {
- description: this.regLookups[index].describe(value),
- address: result,
- name: account && account.name || value
- };
- })
- .filter((data) => data);
- })
- .then((registryValues) => {
- this.registryValues = registryValues;
+ if (account && account.name) {
+ result.name = account.name;
+ } else if (!result.name) {
+ result.name = value;
+ }
+
+ return result;
+ });
});
}
From 6861230bee4f827a89367f58bfa4276000722f4e Mon Sep 17 00:00:00 2001
From: GitLab Build Bot
Date: Wed, 4 Jan 2017 14:24:09 +0000
Subject: [PATCH 14/22] [ci skip] js-precompiled 20170104-142103
---
Cargo.lock | 2 +-
js/package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index d20d65a90..3bb342ca6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1500,7 +1500,7 @@ dependencies = [
[[package]]
name = "parity-ui-precompiled"
version = "1.4.0"
-source = "git+https://github.com/ethcore/js-precompiled.git#ebea2bf78e076916b51b04d8b24187a6a85ae440"
+source = "git+https://github.com/ethcore/js-precompiled.git#567fe36d9db1d120ca7f6fb58f6f66649a3d7f34"
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 8672578c7..8e45c9373 100644
--- a/js/package.json
+++ b/js/package.json
@@ -1,6 +1,6 @@
{
"name": "parity.js",
- "version": "0.2.165",
+ "version": "0.2.166",
"main": "release/index.js",
"jsnext:main": "src/index.js",
"author": "Parity Team ",
From fbb52ac58bc65939e2d4c4c496e1e36e0fd6efe5 Mon Sep 17 00:00:00 2001
From: Nicolas Gotchac
Date: Wed, 4 Jan 2017 16:25:39 +0100
Subject: [PATCH 15/22] Fix wallet in main net (#4038)
---
js/src/views/Wallet/wallet.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/js/src/views/Wallet/wallet.js b/js/src/views/Wallet/wallet.js
index 5418448b4..7a895d6dc 100644
--- a/js/src/views/Wallet/wallet.js
+++ b/js/src/views/Wallet/wallet.js
@@ -180,7 +180,7 @@ class Wallet extends Component {
const { address, isTest, wallet } = this.props;
const { owners, require, confirmations, transactions } = wallet;
- if (!isTest || !owners || !require) {
+ if (!owners || !require) {
return (
From 7ebf8be12e4329a045206933bc9f724d16e5b948 Mon Sep 17 00:00:00 2001
From: GitLab Build Bot
Date: Wed, 4 Jan 2017 15:34:36 +0000
Subject: [PATCH 16/22] [ci skip] js-precompiled 20170104-153136
---
Cargo.lock | 2 +-
js/package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 3bb342ca6..2fee4b290 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1500,7 +1500,7 @@ dependencies = [
[[package]]
name = "parity-ui-precompiled"
version = "1.4.0"
-source = "git+https://github.com/ethcore/js-precompiled.git#567fe36d9db1d120ca7f6fb58f6f66649a3d7f34"
+source = "git+https://github.com/ethcore/js-precompiled.git#9fc9d894356b135c66cc99bf66ec2a713b5f0a8c"
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 8e45c9373..08af16a0a 100644
--- a/js/package.json
+++ b/js/package.json
@@ -1,6 +1,6 @@
{
"name": "parity.js",
- "version": "0.2.166",
+ "version": "0.2.167",
"main": "release/index.js",
"jsnext:main": "src/index.js",
"author": "Parity Team ",
From a4b4263580bcc2d8873a3b3c3f4ec84b689a8b8c Mon Sep 17 00:00:00 2001
From: maciejhirsz
Date: Wed, 4 Jan 2017 16:51:27 +0100
Subject: [PATCH 17/22] Adaptive hints
---
parity/params.rs | 29 ++++++++++++++++++++++++++++-
parity/run.rs | 16 +++++++++-------
2 files changed, 37 insertions(+), 8 deletions(-)
diff --git a/parity/params.rs b/parity/params.rs
index 9399b33e4..93d109979 100644
--- a/parity/params.rs
+++ b/parity/params.rs
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-use std::{str, fs};
+use std::{str, fs, fmt};
use std::time::Duration;
use util::{Address, U256, version_data};
use util::journaldb::Algorithm;
@@ -60,6 +60,21 @@ impl str::FromStr for SpecType {
}
}
+impl fmt::Display for SpecType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str(match *self {
+ SpecType::Mainnet => "homestead",
+ SpecType::Morden => "morden",
+ SpecType::Ropsten => "ropsten",
+ SpecType::Olympic => "olympic",
+ SpecType::Classic => "classic",
+ SpecType::Expanse => "expanse",
+ SpecType::Dev => "dev",
+ SpecType::Custom(ref custom) => custom,
+ })
+ }
+}
+
impl SpecType {
pub fn spec(&self) -> Result {
match *self {
@@ -305,6 +320,18 @@ mod tests {
assert_eq!(SpecType::Mainnet, SpecType::default());
}
+ #[test]
+ fn test_spec_type_display() {
+ assert_eq!(format!("{}", SpecType::Mainnet), "homestead");
+ assert_eq!(format!("{}", SpecType::Ropsten), "ropsten");
+ assert_eq!(format!("{}", SpecType::Morden), "morden");
+ assert_eq!(format!("{}", SpecType::Olympic), "olympic");
+ assert_eq!(format!("{}", SpecType::Classic), "classic");
+ assert_eq!(format!("{}", SpecType::Expanse), "expanse");
+ assert_eq!(format!("{}", SpecType::Dev), "dev");
+ assert_eq!(format!("{}", SpecType::Custom("foo/bar".into())), "foo/bar");
+ }
+
#[test]
fn test_pruning_parsing() {
assert_eq!(Pruning::Auto, "auto".parse().unwrap());
diff --git a/parity/run.rs b/parity/run.rs
index de9d7a639..9c31561eb 100644
--- a/parity/run.rs
+++ b/parity/run.rs
@@ -60,9 +60,6 @@ const SNAPSHOT_PERIOD: u64 = 10000;
// how many blocks to wait before starting a periodic snapshot.
const SNAPSHOT_HISTORY: u64 = 100;
-// Pops along with error message when an account address is not found.
-const CREATE_ACCOUNT_HINT: &'static str = "Please run `parity account new [-d current-d --chain current-chain --keys-path current-keys-path]`.";
-
// Pops along with error messages when a password is missing or invalid.
const VERIFY_PASSWORD_HINT: &'static str = "Make sure valid password is present in files passed using `--password` or in the configuration file.";
@@ -223,7 +220,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R
let passwords = passwords_from_files(&cmd.acc_conf.password_files)?;
// prepare account provider
- let account_provider = Arc::new(prepare_account_provider(&cmd.dirs, &spec.data_dir, cmd.acc_conf, &passwords)?);
+ let account_provider = Arc::new(prepare_account_provider(&cmd.spec, &cmd.dirs, &spec.data_dir, cmd.acc_conf, &passwords)?);
// let the Engine access the accounts
spec.engine.register_account_provider(account_provider.clone());
@@ -240,7 +237,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R
if engine_signer != Default::default() {
// Check if engine signer exists
if !account_provider.has_account(engine_signer).unwrap_or(false) {
- return Err(format!("Consensus signer account not found for the current chain. {}", CREATE_ACCOUNT_HINT));
+ return Err(format!("Consensus signer account not found for the current chain. {}", build_create_account_hint(&cmd.spec, &cmd.dirs.keys)));
}
// Check if any passwords have been read from the password file(s)
@@ -489,7 +486,7 @@ fn daemonize(_pid_file: String) -> Result<(), String> {
Err("daemon is no supported on windows".into())
}
-fn prepare_account_provider(dirs: &Directories, data_dir: &str, cfg: AccountsConfig, passwords: &[String]) -> Result {
+fn prepare_account_provider(spec: &SpecType, dirs: &Directories, data_dir: &str, cfg: AccountsConfig, passwords: &[String]) -> Result {
use ethcore::ethstore::EthStore;
use ethcore::ethstore::dir::DiskDirectory;
@@ -503,7 +500,7 @@ fn prepare_account_provider(dirs: &Directories, data_dir: &str, cfg: AccountsCon
for a in cfg.unlocked_accounts {
// Check if the account exists
if !account_provider.has_account(a).unwrap_or(false) {
- return Err(format!("Account {} not found for the current chain. {}", a, CREATE_ACCOUNT_HINT));
+ return Err(format!("Account {} not found for the current chain. {}", a, build_create_account_hint(spec, &dirs.keys)));
}
// Check if any passwords have been read from the password file(s)
@@ -519,6 +516,11 @@ fn prepare_account_provider(dirs: &Directories, data_dir: &str, cfg: AccountsCon
Ok(account_provider)
}
+// Construct an error `String` with an adaptive hint on how to create an account.
+fn build_create_account_hint(spec: &SpecType, keys: &str) -> String {
+ format!("You can create an account via RPC, UI or `parity account new --chain {} --keys-path {}`.", spec, keys)
+}
+
fn wait_for_exit(
panic_handler: Arc,
_http_server: Option,
From 602a4429cc1d0eaf11e4b9eac61e7e7d38dc7e1e Mon Sep 17 00:00:00 2001
From: Jaco Greeff
Date: Thu, 5 Jan 2017 12:06:35 +0100
Subject: [PATCH 18/22] Account view updates (#4008)
* Fix null account render issue, add tests
* Add tests for #3999 fix (merged in #4000)
* Only include sinon-as-promised globally for mocha
* Move transactions state into tested store
* Add esjify for mocha + ejs (cherry-picked)
* Extract store state into store, test it
* Use address (as per PR comments)
* Fix failing test after master merge
---
js/package.json | 5 +-
js/src/3rdparty/etherscan/account.js | 6 +-
js/src/3rdparty/etherscan/helpers.spec.js | 38 +++
js/src/api/subscriptions/eth.spec.js | 1 -
js/src/api/subscriptions/personal.spec.js | 1 -
js/src/ui/Actionbar/actionbar.js | 3 +-
js/src/ui/Certifications/certifications.js | 6 +-
js/src/ui/Icons/index.js | 8 +-
js/src/views/Account/Header/header.js | 73 +++--
js/src/views/Account/Header/header.spec.js | 156 ++++++++++
js/src/views/Account/Transactions/store.js | 118 ++++++++
.../views/Account/Transactions/store.spec.js | 193 ++++++++++++
.../Account/Transactions/transactions.js | 107 ++-----
.../Account/Transactions/transactions.spec.js | 55 ++++
.../Account/Transactions/transactions.test.js | 31 ++
js/src/views/Account/account.js | 275 +++++++-----------
js/src/views/Account/account.spec.js | 226 ++++++++++++++
js/src/views/Account/account.test.js | 52 ++++
js/src/views/Account/store.js | 50 ++++
js/src/views/Account/store.spec.js | 84 ++++++
js/src/views/Accounts/Summary/summary.js | 2 +-
js/test/mocha.config.js | 1 +
22 files changed, 1210 insertions(+), 281 deletions(-)
create mode 100644 js/src/3rdparty/etherscan/helpers.spec.js
create mode 100644 js/src/views/Account/Header/header.spec.js
create mode 100644 js/src/views/Account/Transactions/store.js
create mode 100644 js/src/views/Account/Transactions/store.spec.js
create mode 100644 js/src/views/Account/Transactions/transactions.spec.js
create mode 100644 js/src/views/Account/Transactions/transactions.test.js
create mode 100644 js/src/views/Account/account.spec.js
create mode 100644 js/src/views/Account/account.test.js
create mode 100644 js/src/views/Account/store.js
create mode 100644 js/src/views/Account/store.spec.js
diff --git a/js/package.json b/js/package.json
index 08af16a0a..29bb70791 100644
--- a/js/package.json
+++ b/js/package.json
@@ -43,8 +43,8 @@
"lint:css": "stylelint ./src/**/*.css",
"lint:js": "eslint --ignore-path .gitignore ./src/",
"lint:js:cached": "eslint --cache --ignore-path .gitignore ./src/",
- "test": "NODE_ENV=test mocha 'src/**/*.spec.js'",
- "test:coverage": "NODE_ENV=test istanbul cover _mocha -- 'src/**/*.spec.js'",
+ "test": "NODE_ENV=test mocha --compilers ejs:ejsify 'src/**/*.spec.js'",
+ "test:coverage": "NODE_ENV=test istanbul cover _mocha -- --compilers ejs:ejsify 'src/**/*.spec.js'",
"test:e2e": "NODE_ENV=test mocha 'src/**/*.e2e.js'",
"test:npm": "(cd .npmjs && npm i) && node test/npmParity && (rm -rf .npmjs/node_modules)",
"prepush": "npm run lint:cached"
@@ -80,6 +80,7 @@
"coveralls": "2.11.15",
"css-loader": "0.26.1",
"ejs-loader": "0.3.0",
+ "ejsify": "1.0.0",
"enzyme": "2.7.0",
"eslint": "3.11.1",
"eslint-config-semistandard": "7.0.0",
diff --git a/js/src/3rdparty/etherscan/account.js b/js/src/3rdparty/etherscan/account.js
index 7b8c431a0..52a08ef4b 100644
--- a/js/src/3rdparty/etherscan/account.js
+++ b/js/src/3rdparty/etherscan/account.js
@@ -49,17 +49,17 @@ function transactions (address, page, test = false) {
// page offset from 0
return _call('txlist', {
address: address,
- page: (page || 0) + 1,
offset: PAGE_SIZE,
+ page: (page || 0) + 1,
sort: 'desc'
}, test).then((transactions) => {
return transactions.map((tx) => {
return {
+ blockNumber: new BigNumber(tx.blockNumber || 0),
from: util.toChecksumAddress(tx.from),
- to: util.toChecksumAddress(tx.to),
hash: tx.hash,
- blockNumber: new BigNumber(tx.blockNumber),
timeStamp: tx.timeStamp,
+ to: util.toChecksumAddress(tx.to),
value: tx.value
};
});
diff --git a/js/src/3rdparty/etherscan/helpers.spec.js b/js/src/3rdparty/etherscan/helpers.spec.js
new file mode 100644
index 000000000..508a7b47a
--- /dev/null
+++ b/js/src/3rdparty/etherscan/helpers.spec.js
@@ -0,0 +1,38 @@
+// Copyright 2015, 2016 Parity Technologies (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 nock from 'nock';
+import { stringify } from 'qs';
+
+import { url } from './links';
+
+function mockget (requests, test) {
+ let scope = nock(url(test));
+
+ requests.forEach((request) => {
+ scope = scope
+ .get(`/api?${stringify(request.query)}`)
+ .reply(request.code || 200, () => {
+ return { result: request.reply };
+ });
+ });
+
+ return scope;
+}
+
+export {
+ mockget
+};
diff --git a/js/src/api/subscriptions/eth.spec.js b/js/src/api/subscriptions/eth.spec.js
index 87cc76d03..680ff881e 100644
--- a/js/src/api/subscriptions/eth.spec.js
+++ b/js/src/api/subscriptions/eth.spec.js
@@ -16,7 +16,6 @@
import BigNumber from 'bignumber.js';
import sinon from 'sinon';
-import 'sinon-as-promised';
import Eth from './eth';
diff --git a/js/src/api/subscriptions/personal.spec.js b/js/src/api/subscriptions/personal.spec.js
index b00354f64..2359192f0 100644
--- a/js/src/api/subscriptions/personal.spec.js
+++ b/js/src/api/subscriptions/personal.spec.js
@@ -15,7 +15,6 @@
// along with Parity. If not, see .
import sinon from 'sinon';
-import 'sinon-as-promised';
import Personal from './personal';
diff --git a/js/src/ui/Actionbar/actionbar.js b/js/src/ui/Actionbar/actionbar.js
index 0141016ab..49cc77df1 100644
--- a/js/src/ui/Actionbar/actionbar.js
+++ b/js/src/ui/Actionbar/actionbar.js
@@ -50,8 +50,7 @@ export default class Actionbar extends Component {
}
return (
-
+
{ buttons }
);
diff --git a/js/src/ui/Certifications/certifications.js b/js/src/ui/Certifications/certifications.js
index bafd06f35..5604ab90a 100644
--- a/js/src/ui/Certifications/certifications.js
+++ b/js/src/ui/Certifications/certifications.js
@@ -25,7 +25,7 @@ import styles from './certifications.css';
class Certifications extends Component {
static propTypes = {
- account: PropTypes.string.isRequired,
+ address: PropTypes.string.isRequired,
certifications: PropTypes.array.isRequired,
dappsUrl: PropTypes.string.isRequired
}
@@ -60,10 +60,10 @@ class Certifications extends Component {
}
function mapStateToProps (_, initProps) {
- const { account } = initProps;
+ const { address } = initProps;
return (state) => {
- const certifications = state.certifications[account] || [];
+ const certifications = state.certifications[address] || [];
const dappsUrl = state.api.dappsUrl;
return { certifications, dappsUrl };
diff --git a/js/src/ui/Icons/index.js b/js/src/ui/Icons/index.js
index 4cf5a2d7d..1e0f93809 100644
--- a/js/src/ui/Icons/index.js
+++ b/js/src/ui/Icons/index.js
@@ -22,13 +22,16 @@ import CompareIcon from 'material-ui/svg-icons/action/compare-arrows';
import ComputerIcon from 'material-ui/svg-icons/hardware/desktop-mac';
import ContractIcon from 'material-ui/svg-icons/action/code';
import DashboardIcon from 'material-ui/svg-icons/action/dashboard';
+import DeleteIcon from 'material-ui/svg-icons/action/delete';
import DoneIcon from 'material-ui/svg-icons/action/done-all';
-import LockedIcon from 'material-ui/svg-icons/action/lock-outline';
+import EditIcon from 'material-ui/svg-icons/content/create';
+import LockedIcon from 'material-ui/svg-icons/action/lock';
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
import SaveIcon from 'material-ui/svg-icons/content/save';
import SendIcon from 'material-ui/svg-icons/content/send';
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
+import VerifyIcon from 'material-ui/svg-icons/action/verified-user';
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';
@@ -41,13 +44,16 @@ export {
ComputerIcon,
ContractIcon,
DashboardIcon,
+ DeleteIcon,
DoneIcon,
+ EditIcon,
LockedIcon,
NextIcon,
PrevIcon,
SaveIcon,
SendIcon,
SnoozeIcon,
+ VerifyIcon,
VisibleIcon,
VpnIcon
};
diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js
index 6e508d05e..f5694177a 100644
--- a/js/src/views/Account/Header/header.js
+++ b/js/src/views/Account/Header/header.js
@@ -15,6 +15,7 @@
// along with Parity. If not, see .
import React, { Component, PropTypes } from 'react';
+import { FormattedMessage } from 'react-intl';
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '~/ui';
import CopyToClipboard from '~/ui/CopyToClipboard';
@@ -26,50 +27,45 @@ export default class Header extends Component {
static propTypes = {
account: PropTypes.object,
balance: PropTypes.object,
- className: PropTypes.string,
children: PropTypes.node,
- isContract: PropTypes.bool,
- hideName: PropTypes.bool
+ className: PropTypes.string,
+ hideName: PropTypes.bool,
+ isContract: PropTypes.bool
};
static defaultProps = {
- className: '',
children: null,
- isContract: false,
- hideName: false
+ className: '',
+ hideName: false,
+ isContract: false
};
render () {
- const { account, balance, className, children, hideName } = this.props;
- const { address, meta, uuid } = account;
+ const { account, balance, children, className, hideName } = this.props;
+
if (!account) {
return null;
}
- const uuidText = !uuid
- ? null
- : uuid: { uuid }
;
+ const { address } = account;
+ const meta = account.meta || {};
return (
-
+
- { this.renderName(address) }
-
+ { this.renderName() }
-
- { uuidText }
+ { this.renderUuid() }
{ meta.description }
{ this.renderTxCount() }
-
@@ -77,9 +73,7 @@ export default class Header extends Component {
-
+
{ children }
@@ -87,15 +81,22 @@ export default class Header extends Component {
);
}
- renderName (address) {
+ renderName () {
const { hideName } = this.props;
if (hideName) {
return null;
}
+ const { address } = this.props.account;
+
return (
- } />
+
+ } />
);
}
@@ -114,7 +115,31 @@ export default class Header extends Component {
return (
- { txCount.toFormat() } outgoing transactions
+
+
+ );
+ }
+
+ renderUuid () {
+ const { uuid } = this.props.account;
+
+ if (!uuid) {
+ return null;
+ }
+
+ return (
+
+
);
}
diff --git a/js/src/views/Account/Header/header.spec.js b/js/src/views/Account/Header/header.spec.js
new file mode 100644
index 000000000..5ae5104d2
--- /dev/null
+++ b/js/src/views/Account/Header/header.spec.js
@@ -0,0 +1,156 @@
+// Copyright 2015, 2016 Parity Technologies (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 BigNumber from 'bignumber.js';
+import { shallow } from 'enzyme';
+import React from 'react';
+
+import Header from './';
+
+const ACCOUNT = {
+ address: '0x0123456789012345678901234567890123456789',
+ meta: {
+ description: 'the description',
+ tags: ['taga', 'tagb']
+ },
+ uuid: '0xabcdef'
+};
+
+let component;
+let instance;
+
+function render (props = {}) {
+ if (props && !props.account) {
+ props.account = ACCOUNT;
+ }
+
+ component = shallow(
+
+ );
+ instance = component.instance();
+
+ return component;
+}
+
+describe('views/Account/Header', () => {
+ describe('rendering', () => {
+ it('renders defaults', () => {
+ expect(render()).to.be.ok;
+ });
+
+ it('renders null with no account', () => {
+ expect(render(null).find('div')).to.have.length(0);
+ });
+
+ it('renders when no account meta', () => {
+ expect(render({ account: { address: ACCOUNT.address } })).to.be.ok;
+ });
+
+ it('renders when no account description', () => {
+ expect(render({ account: { address: ACCOUNT.address, meta: { tags: [] } } })).to.be.ok;
+ });
+
+ it('renders when no account tags', () => {
+ expect(render({ account: { address: ACCOUNT.address, meta: { description: 'something' } } })).to.be.ok;
+ });
+
+ describe('sections', () => {
+ it('renders the Balance', () => {
+ render({ balance: { balance: 'testing' } });
+ const balance = component.find('Connect(Balance)');
+
+ expect(balance).to.have.length(1);
+ expect(balance.props().account).to.deep.equal(ACCOUNT);
+ expect(balance.props().balance).to.deep.equal({ balance: 'testing' });
+ });
+
+ it('renders the Certifications', () => {
+ render();
+ const certs = component.find('Connect(Certifications)');
+
+ expect(certs).to.have.length(1);
+ expect(certs.props().address).to.deep.equal(ACCOUNT.address);
+ });
+
+ it('renders the IdentityIcon', () => {
+ render();
+ const icon = component.find('Connect(IdentityIcon)');
+
+ expect(icon).to.have.length(1);
+ expect(icon.props().address).to.equal(ACCOUNT.address);
+ });
+
+ it('renders the Tags', () => {
+ render();
+ const tags = component.find('Tags');
+
+ expect(tags).to.have.length(1);
+ expect(tags.props().tags).to.deep.equal(ACCOUNT.meta.tags);
+ });
+ });
+ });
+
+ describe('renderName', () => {
+ it('renders null with hideName', () => {
+ render({ hideName: true });
+ expect(instance.renderName()).to.be.null;
+ });
+
+ it('renders the name', () => {
+ render();
+ expect(instance.renderName()).not.to.be.null;
+ });
+
+ it('renders when no address specified', () => {
+ render({ account: {} });
+ expect(instance.renderName()).to.be.ok;
+ });
+ });
+
+ describe('renderTxCount', () => {
+ it('renders null when contract', () => {
+ render({ balance: { txCount: new BigNumber(1) }, isContract: true });
+ expect(instance.renderTxCount()).to.be.null;
+ });
+
+ it('renders null when no balance', () => {
+ render({ balance: null, isContract: false });
+ expect(instance.renderTxCount()).to.be.null;
+ });
+
+ it('renders null when txCount is null', () => {
+ render({ balance: { txCount: null }, isContract: false });
+ expect(instance.renderTxCount()).to.be.null;
+ });
+
+ it('renders the tx count', () => {
+ render({ balance: { txCount: new BigNumber(1) }, isContract: false });
+ expect(instance.renderTxCount()).not.to.be.null;
+ });
+ });
+
+ describe('renderUuid', () => {
+ it('renders null with no uuid', () => {
+ render({ account: Object.assign({}, ACCOUNT, { uuid: null }) });
+ expect(instance.renderUuid()).to.be.null;
+ });
+
+ it('renders the uuid', () => {
+ render();
+ expect(instance.renderUuid()).not.to.be.null;
+ });
+ });
+});
diff --git a/js/src/views/Account/Transactions/store.js b/js/src/views/Account/Transactions/store.js
new file mode 100644
index 000000000..d59595c44
--- /dev/null
+++ b/js/src/views/Account/Transactions/store.js
@@ -0,0 +1,118 @@
+// Copyright 2015, 2016 Parity Technologies (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, transaction } from 'mobx';
+
+import etherscan from '~/3rdparty/etherscan';
+
+export default class Store {
+ @observable address = null;
+ @observable isLoading = false;
+ @observable isTest = undefined;
+ @observable isTracing = false;
+ @observable txHashes = [];
+
+ constructor (api) {
+ this._api = api;
+ }
+
+ @action setHashes = (transactions) => {
+ transaction(() => {
+ this.setLoading(false);
+ this.txHashes = transactions.map((transaction) => transaction.hash);
+ });
+ }
+
+ @action setAddress = (address) => {
+ this.address = address;
+ }
+
+ @action setLoading = (isLoading) => {
+ this.isLoading = isLoading;
+ }
+
+ @action setTest = (isTest) => {
+ this.isTest = isTest;
+ }
+
+ @action setTracing = (isTracing) => {
+ this.isTracing = isTracing;
+ }
+
+ @action updateProps = (props) => {
+ transaction(() => {
+ this.setAddress(props.address);
+ this.setTest(props.isTest);
+
+ // TODO: When tracing is enabled again, adjust to actually set
+ this.setTracing(false && props.traceMode);
+ });
+
+ return this.getTransactions();
+ }
+
+ getTransactions () {
+ if (this.isTest === undefined) {
+ return Promise.resolve();
+ }
+
+ this.setLoading(true);
+
+ // TODO: When supporting other chains (eg. ETC). call to be made to other endpoints
+ return (
+ this.isTracing
+ ? this.fetchTraceTransactions()
+ : this.fetchEtherscanTransactions()
+ )
+ .then((transactions) => {
+ this.setHashes(transactions);
+ })
+ .catch((error) => {
+ console.warn('getTransactions', error);
+ this.setLoading(false);
+ });
+ }
+
+ fetchEtherscanTransactions () {
+ return etherscan.account.transactions(this.address, 0, this.isTest);
+ }
+
+ fetchTraceTransactions () {
+ return Promise
+ .all([
+ this._api.trace.filter({
+ fromAddress: this.address,
+ fromBlock: 0
+ }),
+ this._api.trace.filter({
+ fromBlock: 0,
+ toAddress: this.address
+ })
+ ])
+ .then(([fromTransactions, toTransactions]) => {
+ return fromTransactions
+ .concat(toTransactions)
+ .map((transaction) => {
+ return {
+ blockNumber: transaction.blockNumber,
+ from: transaction.action.from,
+ hash: transaction.transactionHash,
+ to: transaction.action.to
+ };
+ });
+ });
+ }
+}
diff --git a/js/src/views/Account/Transactions/store.spec.js b/js/src/views/Account/Transactions/store.spec.js
new file mode 100644
index 000000000..a25b58d29
--- /dev/null
+++ b/js/src/views/Account/Transactions/store.spec.js
@@ -0,0 +1,193 @@
+// Copyright 2015, 2016 Parity Technologies (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 BigNumber from 'bignumber.js';
+import sinon from 'sinon';
+
+import { mockget as mockEtherscan } from '~/3rdparty/etherscan/helpers.spec.js';
+import { ADDRESS, createApi } from './transactions.test.js';
+
+import Store from './store';
+
+let api;
+let store;
+
+function createStore () {
+ api = createApi();
+ store = new Store(api);
+
+ return store;
+}
+
+function mockQuery () {
+ mockEtherscan([{
+ query: {
+ module: 'account',
+ action: 'txlist',
+ address: ADDRESS,
+ offset: 25,
+ page: 1,
+ sort: 'desc'
+ },
+ reply: [{ hash: '123' }]
+ }], true);
+}
+
+describe('views/Account/Transactions/store', () => {
+ beforeEach(() => {
+ mockQuery();
+ createStore();
+ });
+
+ describe('constructor', () => {
+ it('sets the api', () => {
+ expect(store._api).to.deep.equals(api);
+ });
+
+ it('starts with isLoading === false', () => {
+ expect(store.isLoading).to.be.false;
+ });
+
+ it('starts with isTracing === false', () => {
+ expect(store.isTracing).to.be.false;
+ });
+ });
+
+ describe('@action', () => {
+ describe('setHashes', () => {
+ it('clears the loading state', () => {
+ store.setLoading(true);
+ store.setHashes([]);
+ expect(store.isLoading).to.be.false;
+ });
+
+ it('sets the hashes from the transactions', () => {
+ store.setHashes([{ hash: '123' }, { hash: '456' }]);
+ expect(store.txHashes.peek()).to.deep.equal(['123', '456']);
+ });
+ });
+
+ describe('setAddress', () => {
+ it('sets the address', () => {
+ store.setAddress(ADDRESS);
+ expect(store.address).to.equal(ADDRESS);
+ });
+ });
+
+ describe('setLoading', () => {
+ it('sets the isLoading flag', () => {
+ store.setLoading(true);
+ expect(store.isLoading).to.be.true;
+ });
+ });
+
+ describe('setTest', () => {
+ it('sets the isTest flag', () => {
+ store.setTest(true);
+ expect(store.isTest).to.be.true;
+ });
+ });
+
+ describe('setTracing', () => {
+ it('sets the isTracing flag', () => {
+ store.setTracing(true);
+ expect(store.isTracing).to.be.true;
+ });
+ });
+
+ describe('updateProps', () => {
+ it('retrieves transactions once updated', () => {
+ sinon.spy(store, 'getTransactions');
+ store.updateProps({});
+
+ expect(store.getTransactions).to.have.been.called;
+ store.getTransactions.restore();
+ });
+ });
+ });
+
+ describe('operations', () => {
+ describe('getTransactions', () => {
+ it('retrieves the hashes via etherscan', () => {
+ sinon.spy(store, 'fetchEtherscanTransactions');
+ store.setAddress(ADDRESS);
+ store.setTest(true);
+ store.setTracing(false);
+
+ return store.getTransactions().then(() => {
+ expect(store.fetchEtherscanTransactions).to.have.been.called;
+ expect(store.txHashes.peek()).to.deep.equal(['123']);
+ store.fetchEtherscanTransactions.restore();
+ });
+ });
+
+ it('retrieves the hashes via tracing', () => {
+ sinon.spy(store, 'fetchTraceTransactions');
+ store.setAddress(ADDRESS);
+ store.setTest(true);
+ store.setTracing(true);
+
+ return store.getTransactions().then(() => {
+ expect(store.fetchTraceTransactions).to.have.been.called;
+ expect(store.txHashes.peek()).to.deep.equal(['123', '098']);
+ store.fetchTraceTransactions.restore();
+ });
+ });
+ });
+
+ describe('fetchEtherscanTransactions', () => {
+ it('retrieves the transactions', () => {
+ store.setAddress(ADDRESS);
+ store.setTest(true);
+
+ return store.fetchEtherscanTransactions().then((transactions) => {
+ expect(transactions).to.deep.equal([{
+ blockNumber: new BigNumber(0),
+ from: '',
+ hash: '123',
+ timeStamp: undefined,
+ to: '',
+ value: undefined
+ }]);
+ });
+ });
+ });
+
+ describe('fetchTraceTransactions', () => {
+ it('retrieves the transactions', () => {
+ store.setAddress(ADDRESS);
+ store.setTest(true);
+
+ return store.fetchTraceTransactions().then((transactions) => {
+ expect(transactions).to.deep.equal([
+ {
+ blockNumber: undefined,
+ from: undefined,
+ hash: '123',
+ to: undefined
+ },
+ {
+ blockNumber: undefined,
+ from: undefined,
+ hash: '098',
+ to: undefined
+ }
+ ]);
+ });
+ });
+ });
+ });
+});
diff --git a/js/src/views/Account/Transactions/transactions.js b/js/src/views/Account/Transactions/transactions.js
index eb11e8def..5e48d5c5c 100644
--- a/js/src/views/Account/Transactions/transactions.js
+++ b/js/src/views/Account/Transactions/transactions.js
@@ -14,15 +14,18 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
+import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
+import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
-import etherscan from '~/3rdparty/etherscan';
import { Container, TxList, Loading } from '~/ui';
+import Store from './store';
import styles from './transactions.css';
+@observer
class Transactions extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
@@ -34,34 +37,35 @@ class Transactions extends Component {
traceMode: PropTypes.bool
}
- state = {
- hashes: [],
- loading: true,
- callInfo: {}
- }
+ store = new Store(this.context.api);
- componentDidMount () {
- this.getTransactions(this.props);
+ componentWillMount () {
+ this.store.updateProps(this.props);
}
componentWillReceiveProps (newProps) {
if (this.props.traceMode === undefined && newProps.traceMode !== undefined) {
- this.getTransactions(newProps);
+ this.store.updateProps(newProps);
return;
}
- const hasChanged = [ 'isTest', 'address' ]
+ const hasChanged = ['isTest', 'address']
.map(key => newProps[key] !== this.props[key])
.reduce((truth, keyTruth) => truth || keyTruth, false);
if (hasChanged) {
- this.getTransactions(newProps);
+ this.store.updateProps(newProps);
}
}
render () {
return (
-
+
+ }>
{ this.renderTransactionList() }
{ this.renderEtherscanFooter() }
@@ -69,10 +73,9 @@ class Transactions extends Component {
}
renderTransactionList () {
- const { address } = this.props;
- const { hashes, loading } = this.state;
+ const { address, isLoading, txHashes } = this.store;
- if (loading) {
+ if (isLoading) {
return (
);
@@ -81,85 +84,29 @@ class Transactions extends Component {
return (
);
}
renderEtherscanFooter () {
- const { traceMode } = this.props;
+ const { isTracing } = this.store;
- if (traceMode) {
+ if (isTracing) {
return null;
}
return (
- Transaction list powered by
etherscan.io
+
etherscan.io
+ } } />
);
}
-
- 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) => {
- this.setState({
- hashes: transactions.map((transaction) => transaction.hash),
- loading: false
- });
- });
- }
-
- fetchTransactions = (isTest, address, traceMode) => {
- // if (traceMode) {
- // return this.fetchTraceTransactions(address);
- // }
-
- return this.fetchEtherscanTransactions(isTest, address);
- }
-
- fetchEtherscanTransactions = (isTest, address) => {
- return etherscan.account
- .transactions(address, 0, isTest)
- .catch((error) => {
- console.error('getTransactions', error);
- });
- }
-
- fetchTraceTransactions = (address) => {
- return Promise
- .all([
- this.context.api.trace
- .filter({
- fromBlock: 0,
- fromAddress: address
- }),
- this.context.api.trace
- .filter({
- fromBlock: 0,
- toAddress: address
- })
- ])
- .then(([fromTransactions, toTransactions]) => {
- const transactions = [].concat(fromTransactions, toTransactions);
-
- return transactions.map(transaction => ({
- from: transaction.action.from,
- to: transaction.action.to,
- blockNumber: transaction.blockNumber,
- hash: transaction.transactionHash
- }));
- });
- }
}
function mapStateToProps (state) {
diff --git a/js/src/views/Account/Transactions/transactions.spec.js b/js/src/views/Account/Transactions/transactions.spec.js
new file mode 100644
index 000000000..53f55b524
--- /dev/null
+++ b/js/src/views/Account/Transactions/transactions.spec.js
@@ -0,0 +1,55 @@
+// Copyright 2015, 2016 Parity Technologies (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 { shallow } from 'enzyme';
+import React from 'react';
+
+import { ADDRESS, createApi, createRedux } from './transactions.test.js';
+
+import Transactions from './';
+
+let component;
+let instance;
+
+function render (props) {
+ component = shallow(
+ ,
+ { context: { store: createRedux() } }
+ ).find('Transactions').shallow({ context: { api: createApi() } });
+ instance = component.instance();
+
+ return component;
+}
+
+describe('views/Account/Transactions', () => {
+ it('renders defaults', () => {
+ expect(render()).to.be.ok;
+ });
+
+ describe('renderTransactionList', () => {
+ it('renders Loading when isLoading === true', () => {
+ instance.store.setLoading(true);
+ expect(instance.renderTransactionList().type).to.match(/Loading/);
+ });
+
+ it('renders TxList when isLoading === true', () => {
+ instance.store.setLoading(false);
+ expect(instance.renderTransactionList().type).to.match(/Connect/);
+ });
+ });
+});
diff --git a/js/src/views/Account/Transactions/transactions.test.js b/js/src/views/Account/Transactions/transactions.test.js
new file mode 100644
index 000000000..4b7b679b6
--- /dev/null
+++ b/js/src/views/Account/Transactions/transactions.test.js
@@ -0,0 +1,31 @@
+// Copyright 2015, 2016 Parity Technologies (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 { ADDRESS, createRedux } from '../account.test.js';
+
+function createApi () {
+ return {
+ trace: {
+ filter: (options) => Promise.resolve([{ transactionHash: options.fromAddress ? '123' : '098', action: {} }])
+ }
+ };
+}
+
+export {
+ ADDRESS,
+ createApi,
+ createRedux
+};
diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js
index f274d8fbe..e3c4d9776 100644
--- a/js/src/views/Account/account.js
+++ b/js/src/views/Account/account.js
@@ -14,47 +14,38 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
+import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
+import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
-import ActionDelete from 'material-ui/svg-icons/action/delete';
-import ContentCreate from 'material-ui/svg-icons/content/create';
-import ContentSend from 'material-ui/svg-icons/content/send';
-import LockIcon from 'material-ui/svg-icons/action/lock';
-import VerifyIcon from 'material-ui/svg-icons/action/verified-user';
-
-import { EditMeta, DeleteAccount, Shapeshift, Verification, Transfer, PasswordManager } from '~/modals';
-import { Actionbar, Button, Page } from '~/ui';
import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png';
-
-import Header from './Header';
-import Transactions from './Transactions';
+import { EditMeta, DeleteAccount, Shapeshift, Verification, Transfer, PasswordManager } from '~/modals';
import { setVisibleAccounts } from '~/redux/providers/personalActions';
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
+import { Actionbar, Button, Page } from '~/ui';
+import { DeleteIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon } from '~/ui/Icons';
+import Header from './Header';
+import Store from './store';
+import Transactions from './Transactions';
import styles from './account.css';
+@observer
class Account extends Component {
static propTypes = {
- setVisibleAccounts: PropTypes.func.isRequired,
fetchCertifiers: PropTypes.func.isRequired,
fetchCertifications: PropTypes.func.isRequired,
images: PropTypes.object.isRequired,
+ setVisibleAccounts: PropTypes.func.isRequired,
- params: PropTypes.object,
accounts: PropTypes.object,
- balances: PropTypes.object
+ balances: PropTypes.object,
+ params: PropTypes.object
}
- state = {
- showDeleteDialog: false,
- showEditDialog: false,
- showFundDialog: false,
- showVerificationDialog: false,
- showTransferDialog: false,
- showPasswordDialog: false
- }
+ store = new Store();
componentDidMount () {
this.props.fetchCertifiers();
@@ -76,7 +67,8 @@ class Account extends Component {
setVisibleAccounts (props = this.props) {
const { params, setVisibleAccounts, fetchCertifications } = props;
- const addresses = [ params.address ];
+ const addresses = [params.address];
+
setVisibleAccounts(addresses);
fetchCertifications(params.address);
}
@@ -97,15 +89,14 @@ class Account extends Component {
{ this.renderDeleteDialog(account) }
{ this.renderEditDialog(account) }
{ this.renderFundDialog() }
+ { this.renderPasswordDialog(account) }
+ { this.renderTransferDialog(account, balance) }
{ this.renderVerificationDialog() }
- { this.renderTransferDialog() }
- { this.renderPasswordDialog() }
- { this.renderActionbar() }
+ { this.renderActionbar(balance) }
+ balance={ balance } />
@@ -114,86 +105,108 @@ class Account extends Component {
);
}
- renderActionbar () {
- const { address } = this.props.params;
- const { balances } = this.props;
- const balance = balances[address];
-
+ renderActionbar (balance) {
const showTransferButton = !!(balance && balance.tokens);
const buttons = [
}
- label='transfer'
disabled={ !showTransferButton }
- onClick={ this.onTransferClick } />,
+ icon={ }
+ key='transferFunds'
+ label={
+
+ }
+ onClick={ this.store.toggleTransferDialog } />,
+ }
key='shapeshift'
- icon={
}
- label='shapeshift'
- onClick={ this.onShapeshiftAccountClick } />,
+ label={
+
+ }
+ onClick={ this.store.toggleFundDialog } />,
}
- label='Verify'
- onClick={ this.openVerification } />,
+ key='sms-verification'
+ label={
+
+ }
+ onClick={ this.store.toggleVerificationDialog } />,
}
key='editmeta'
- icon={ }
- label='edit'
- onClick={ this.onEditClick } />,
+ label={
+
+ }
+ onClick={ this.store.toggleEditDialog } />,
}
key='passwordManager'
- icon={ }
- label='password'
- onClick={ this.onPasswordClick } />,
+ label={
+
+ }
+ onClick={ this.store.togglePasswordDialog } />,
}
key='delete'
- icon={ }
- label='delete account'
- onClick={ this.onDeleteClick } />
+ label={
+
+ }
+ onClick={ this.store.toggleDeleteDialog } />
];
return (
+ buttons={ buttons }
+ title={
+
+ } />
);
}
renderDeleteDialog (account) {
- const { showDeleteDialog } = this.state;
-
- if (!showDeleteDialog) {
+ if (!this.store.isDeleteVisible) {
return null;
}
return (
+ onClose={ this.store.toggleDeleteDialog } />
);
}
renderEditDialog (account) {
- const { showEditDialog } = this.state;
-
- if (!showEditDialog) {
+ if (!this.store.isEditVisible) {
return null;
}
return (
+ onClose={ this.store.toggleEditDialog } />
);
}
renderFundDialog () {
- const { showFundDialog } = this.state;
-
- if (!showFundDialog) {
+ if (!this.store.isFundVisible) {
return null;
}
@@ -202,12 +215,41 @@ class Account extends Component {
return (
+ onClose={ this.store.toggleFundDialog } />
+ );
+ }
+
+ renderPasswordDialog (account) {
+ if (!this.store.isPasswordVisible) {
+ return null;
+ }
+
+ return (
+
+ );
+ }
+
+ renderTransferDialog (account, balance) {
+ if (!this.store.isTransferVisible) {
+ return null;
+ }
+
+ const { balances, images } = this.props;
+
+ return (
+
);
}
renderVerificationDialog () {
- if (!this.state.showVerificationDialog) {
+ if (!this.store.isVerificationVisible) {
return null;
}
@@ -216,102 +258,9 @@ class Account extends Component {
return (
+ onClose={ this.store.toggleVerificationDialog } />
);
}
-
- renderTransferDialog () {
- const { showTransferDialog } = this.state;
-
- if (!showTransferDialog) {
- return null;
- }
-
- const { address } = this.props.params;
- const { accounts, balances, images } = this.props;
- const account = accounts[address];
- const balance = balances[address];
-
- return (
-
- );
- }
-
- renderPasswordDialog () {
- const { showPasswordDialog } = this.state;
-
- if (!showPasswordDialog) {
- return null;
- }
-
- const { address } = this.props.params;
- const { accounts } = this.props;
- const account = accounts[address];
-
- return (
-
- );
- }
-
- onDeleteClick = () => {
- this.setState({ showDeleteDialog: true });
- }
-
- onDeleteClose = () => {
- this.setState({ showDeleteDialog: false });
- }
-
- onEditClick = () => {
- this.setState({
- showEditDialog: !this.state.showEditDialog
- });
- }
-
- onShapeshiftAccountClick = () => {
- this.setState({
- showFundDialog: !this.state.showFundDialog
- });
- }
-
- onShapeshiftAccountClose = () => {
- this.onShapeshiftAccountClick();
- }
-
- openVerification = () => {
- this.setState({ showVerificationDialog: true });
- }
-
- onVerificationClose = () => {
- this.setState({ showVerificationDialog: false });
- }
-
- onTransferClick = () => {
- this.setState({
- showTransferDialog: !this.state.showTransferDialog
- });
- }
-
- onTransferClose = () => {
- this.onTransferClick();
- }
-
- onPasswordClick = () => {
- this.setState({
- showPasswordDialog: !this.state.showPasswordDialog
- });
- }
-
- onPasswordClose = () => {
- this.onPasswordClick();
- }
}
function mapStateToProps (state) {
@@ -328,9 +277,9 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) {
return bindActionCreators({
- setVisibleAccounts,
fetchCertifiers,
- fetchCertifications
+ fetchCertifications,
+ setVisibleAccounts
}, dispatch);
}
diff --git a/js/src/views/Account/account.spec.js b/js/src/views/Account/account.spec.js
new file mode 100644
index 000000000..33ca89588
--- /dev/null
+++ b/js/src/views/Account/account.spec.js
@@ -0,0 +1,226 @@
+// Copyright 2015, 2016 Parity Technologies (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 { shallow } from 'enzyme';
+import React from 'react';
+
+import { ADDRESS, createRedux } from './account.test.js';
+
+import Account from './';
+
+let component;
+let instance;
+let store;
+
+function render (props) {
+ component = shallow(
+ ,
+ { context: { store: createRedux() } }
+ ).find('Account').shallow();
+ instance = component.instance();
+ store = instance.store;
+
+ return component;
+}
+
+describe('views/Account', () => {
+ describe('rendering', () => {
+ beforeEach(() => {
+ render();
+ });
+
+ it('renders defaults', () => {
+ expect(component).to.be.ok;
+ });
+
+ describe('sections', () => {
+ it('renders the Actionbar', () => {
+ expect(component.find('Actionbar')).to.have.length(1);
+ });
+
+ it('renders the Page', () => {
+ expect(component.find('Page')).to.have.length(1);
+ });
+
+ it('renders the Header', () => {
+ expect(component.find('Header')).to.have.length(1);
+ });
+
+ it('renders the Transactions', () => {
+ expect(component.find('Connect(Transactions)')).to.have.length(1);
+ });
+
+ it('renders no other sections', () => {
+ expect(component.find('div').children()).to.have.length(2);
+ });
+ });
+ });
+
+ describe('sub-renderers', () => {
+ describe('renderActionBar', () => {
+ let bar;
+ let barShallow;
+
+ beforeEach(() => {
+ render();
+
+ bar = instance.renderActionbar({ tokens: {} });
+ barShallow = shallow(bar);
+ });
+
+ it('renders the bar', () => {
+ expect(bar.type).to.match(/Actionbar/);
+ });
+
+ // TODO: Finding by index is not optimal, however couldn't find a better method atm
+ // since we cannot find by key (prop not visible in shallow debug())
+ describe('clicks', () => {
+ it('toggles transfer on click', () => {
+ barShallow.find('Button').at(0).simulate('click');
+ expect(store.isTransferVisible).to.be.true;
+ });
+
+ it('toggles fund on click', () => {
+ barShallow.find('Button').at(1).simulate('click');
+ expect(store.isFundVisible).to.be.true;
+ });
+
+ it('toggles fund on click', () => {
+ barShallow.find('Button').at(1).simulate('click');
+ expect(store.isFundVisible).to.be.true;
+ });
+
+ it('toggles verify on click', () => {
+ barShallow.find('Button').at(2).simulate('click');
+ expect(store.isVerificationVisible).to.be.true;
+ });
+
+ it('toggles edit on click', () => {
+ barShallow.find('Button').at(3).simulate('click');
+ expect(store.isEditVisible).to.be.true;
+ });
+
+ it('toggles password on click', () => {
+ barShallow.find('Button').at(4).simulate('click');
+ expect(store.isPasswordVisible).to.be.true;
+ });
+
+ it('toggles delete on click', () => {
+ barShallow.find('Button').at(5).simulate('click');
+ expect(store.isDeleteVisible).to.be.true;
+ });
+ });
+ });
+
+ describe('renderDeleteDialog', () => {
+ it('renders null when not visible', () => {
+ render();
+
+ expect(store.isDeleteVisible).to.be.false;
+ expect(instance.renderDeleteDialog()).to.be.null;
+ });
+
+ it('renders the modal when visible', () => {
+ render();
+
+ store.toggleDeleteDialog();
+ expect(instance.renderDeleteDialog().type).to.match(/Connect/);
+ });
+ });
+
+ describe('renderEditDialog', () => {
+ it('renders null when not visible', () => {
+ render();
+
+ expect(store.isEditVisible).to.be.false;
+ expect(instance.renderEditDialog()).to.be.null;
+ });
+
+ it('renders the modal when visible', () => {
+ render();
+
+ store.toggleEditDialog();
+ expect(instance.renderEditDialog({ address: ADDRESS }).type).to.match(/Connect/);
+ });
+ });
+
+ describe('renderFundDialog', () => {
+ it('renders null when not visible', () => {
+ render();
+
+ expect(store.isFundVisible).to.be.false;
+ expect(instance.renderFundDialog()).to.be.null;
+ });
+
+ it('renders the modal when visible', () => {
+ render();
+
+ store.toggleFundDialog();
+ expect(instance.renderFundDialog().type).to.match(/Shapeshift/);
+ });
+ });
+
+ describe('renderPasswordDialog', () => {
+ it('renders null when not visible', () => {
+ render();
+
+ expect(store.isPasswordVisible).to.be.false;
+ expect(instance.renderPasswordDialog()).to.be.null;
+ });
+
+ it('renders the modal when visible', () => {
+ render();
+
+ store.togglePasswordDialog();
+ expect(instance.renderPasswordDialog({ address: ADDRESS }).type).to.match(/Connect/);
+ });
+ });
+
+ describe('renderTransferDialog', () => {
+ it('renders null when not visible', () => {
+ render();
+
+ expect(store.isTransferVisible).to.be.false;
+ expect(instance.renderTransferDialog()).to.be.null;
+ });
+
+ it('renders the modal when visible', () => {
+ render();
+
+ store.toggleTransferDialog();
+ expect(instance.renderTransferDialog().type).to.match(/Connect/);
+ });
+ });
+
+ describe('renderVerificationDialog', () => {
+ it('renders null when not visible', () => {
+ render();
+
+ expect(store.isVerificationVisible).to.be.false;
+ expect(instance.renderVerificationDialog()).to.be.null;
+ });
+
+ it('renders the modal when visible', () => {
+ render();
+
+ store.toggleVerificationDialog();
+ expect(instance.renderVerificationDialog().type).to.match(/Connect/);
+ });
+ });
+ });
+});
diff --git a/js/src/views/Account/account.test.js b/js/src/views/Account/account.test.js
new file mode 100644
index 000000000..d457bf7a1
--- /dev/null
+++ b/js/src/views/Account/account.test.js
@@ -0,0 +1,52 @@
+// Copyright 2015, 2016 Parity Technologies (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 sinon from 'sinon';
+
+const ADDRESS = '0x0123456789012345678901234567890123456789';
+
+function createRedux () {
+ return {
+ dispatch: sinon.stub(),
+ subscribe: sinon.stub(),
+ getState: () => {
+ return {
+ balances: {
+ balances: {
+ [ADDRESS]: {}
+ }
+ },
+ images: {},
+ nodeStatus: {
+ isTest: false,
+ traceMode: false
+ },
+ personal: {
+ accounts: {
+ [ADDRESS]: {
+ address: ADDRESS
+ }
+ }
+ }
+ };
+ }
+ };
+}
+
+export {
+ ADDRESS,
+ createRedux
+};
diff --git a/js/src/views/Account/store.js b/js/src/views/Account/store.js
new file mode 100644
index 000000000..e7655e9d7
--- /dev/null
+++ b/js/src/views/Account/store.js
@@ -0,0 +1,50 @@
+// Copyright 2015, 2016 Parity Technologies (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';
+
+export default class Store {
+ @observable isDeleteVisible = false;
+ @observable isEditVisible = false;
+ @observable isFundVisible = false;
+ @observable isPasswordVisible = false;
+ @observable isTransferVisible = false;
+ @observable isVerificationVisible = false;
+
+ @action toggleDeleteDialog = () => {
+ this.isDeleteVisible = !this.isDeleteVisible;
+ }
+
+ @action toggleEditDialog = () => {
+ this.isEditVisible = !this.isEditVisible;
+ }
+
+ @action toggleFundDialog = () => {
+ this.isFundVisible = !this.isFundVisible;
+ }
+
+ @action togglePasswordDialog = () => {
+ this.isPasswordVisible = !this.isPasswordVisible;
+ }
+
+ @action toggleTransferDialog = () => {
+ this.isTransferVisible = !this.isTransferVisible;
+ }
+
+ @action toggleVerificationDialog = () => {
+ this.isVerificationVisible = !this.isVerificationVisible;
+ }
+}
diff --git a/js/src/views/Account/store.spec.js b/js/src/views/Account/store.spec.js
new file mode 100644
index 000000000..7035b97a7
--- /dev/null
+++ b/js/src/views/Account/store.spec.js
@@ -0,0 +1,84 @@
+// Copyright 2015, 2016 Parity Technologies (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 Store from './store';
+
+let store;
+
+function createStore () {
+ store = new Store();
+}
+
+describe('views/Account/Store', () => {
+ beforeEach(() => {
+ createStore();
+ });
+
+ describe('constructor', () => {
+ it('sets all modal visibility to false', () => {
+ expect(store.isDeleteVisible).to.be.false;
+ expect(store.isEditVisible).to.be.false;
+ expect(store.isFundVisible).to.be.false;
+ expect(store.isPasswordVisible).to.be.false;
+ expect(store.isTransferVisible).to.be.false;
+ expect(store.isVerificationVisible).to.be.false;
+ });
+ });
+
+ describe('@action', () => {
+ describe('toggleDeleteDialog', () => {
+ it('toggles the visibility', () => {
+ store.toggleDeleteDialog();
+ expect(store.isDeleteVisible).to.be.true;
+ });
+ });
+
+ describe('toggleEditDialog', () => {
+ it('toggles the visibility', () => {
+ store.toggleEditDialog();
+ expect(store.isEditVisible).to.be.true;
+ });
+ });
+
+ describe('toggleFundDialog', () => {
+ it('toggles the visibility', () => {
+ store.toggleFundDialog();
+ expect(store.isFundVisible).to.be.true;
+ });
+ });
+
+ describe('togglePasswordDialog', () => {
+ it('toggles the visibility', () => {
+ store.togglePasswordDialog();
+ expect(store.isPasswordVisible).to.be.true;
+ });
+ });
+
+ describe('toggleTransferDialog', () => {
+ it('toggles the visibility', () => {
+ store.toggleTransferDialog();
+ expect(store.isTransferVisible).to.be.true;
+ });
+ });
+
+ describe('toggleVerificationDialog', () => {
+ it('toggles the visibility', () => {
+ store.toggleVerificationDialog();
+ expect(store.isVerificationVisible).to.be.true;
+ });
+ });
+ });
+});
diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js
index 55e868c08..8658077a5 100644
--- a/js/src/views/Accounts/Summary/summary.js
+++ b/js/src/views/Accounts/Summary/summary.js
@@ -197,7 +197,7 @@ export default class Summary extends Component {
}
return (
-
+
);
}
}
diff --git a/js/test/mocha.config.js b/js/test/mocha.config.js
index 3201cd4ac..2ab58455f 100644
--- a/js/test/mocha.config.js
+++ b/js/test/mocha.config.js
@@ -26,6 +26,7 @@ injectTapEventPlugin();
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import chaiEnzyme from 'chai-enzyme';
+import 'sinon-as-promised';
import sinonChai from 'sinon-chai';
import { WebSocket } from 'mock-socket';
import jsdom from 'jsdom';
From ddeb06d9cc14473a1fa0845b2bea287510bd0935 Mon Sep 17 00:00:00 2001
From: Jaco Greeff
Date: Thu, 5 Jan 2017 12:06:46 +0100
Subject: [PATCH 19/22] UI component updates (#4010)
* Update blockStatus & test results
* IdentityIcon rendering tests for #3950
* Update IdentityName with external messages
* Expand to cover basic layout sections
* ConfirmDialog rendering tests
* TxHash expansion & tests
* Cleanup ui/*.spec.js PropType warnings
* Use react-intl plural for confirmation/confirmations (verified manually)
---
js/src/api/util/identity.js | 2 +-
js/src/ui/BlockStatus/blockStatus.js | 47 ++++--
js/src/ui/BlockStatus/blockStatus.spec.js | 94 +++++++++++
js/src/ui/Button/button.spec.js | 10 +-
js/src/ui/ConfirmDialog/confirmDialog.js | 29 +++-
js/src/ui/ConfirmDialog/confirmDialog.spec.js | 157 ++++++++++++++++++
js/src/ui/Container/container.spec.js | 19 ++-
.../ui/GasPriceEditor/gasPriceEditor.spec.js | 1 +
js/src/ui/IdentityIcon/identityIcon.js | 2 +-
js/src/ui/IdentityIcon/identityIcon.spec.js | 120 +++++++++++++
js/src/ui/IdentityName/identityName.js | 14 +-
js/src/ui/IdentityName/identityName.spec.js | 14 +-
js/src/ui/TxHash/txHash.js | 70 ++++----
js/src/ui/TxHash/txHash.spec.js | 132 +++++++++++++++
js/src/ui/TxList/TxRow/txRow.spec.js | 6 +-
js/src/ui/TxList/txList.spec.js | 4 +-
16 files changed, 652 insertions(+), 69 deletions(-)
create mode 100644 js/src/ui/BlockStatus/blockStatus.spec.js
create mode 100644 js/src/ui/ConfirmDialog/confirmDialog.spec.js
create mode 100644 js/src/ui/IdentityIcon/identityIcon.spec.js
create mode 100644 js/src/ui/TxHash/txHash.spec.js
diff --git a/js/src/api/util/identity.js b/js/src/api/util/identity.js
index e4a95891f..48a683989 100644
--- a/js/src/api/util/identity.js
+++ b/js/src/api/util/identity.js
@@ -21,7 +21,7 @@ const TEST_ENV = process.env.NODE_ENV === 'test';
export function createIdentityImg (address, scale = 8) {
return TEST_ENV
- ? ''
+ ? 'test-createIdentityImg'
: blockies({
seed: (address || '').toLowerCase(),
size: 8,
diff --git a/js/src/ui/BlockStatus/blockStatus.js b/js/src/ui/BlockStatus/blockStatus.js
index f50c7a685..47ee1a1c8 100644
--- a/js/src/ui/BlockStatus/blockStatus.js
+++ b/js/src/ui/BlockStatus/blockStatus.js
@@ -15,6 +15,7 @@
// along with Parity. If not, see .
import React, { Component, PropTypes } from 'react';
+import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
@@ -39,7 +40,12 @@ class BlockStatus extends Component {
if (!syncing) {
return (
- { blockNumber.toFormat() } best block
+
);
}
@@ -47,26 +53,45 @@ class BlockStatus extends Component {
if (syncing.warpChunksAmount && syncing.warpChunksProcessed && !syncing.warpChunksAmount.eq(syncing.warpChunksProcessed)) {
return (
- { syncing.warpChunksProcessed.mul(100).div(syncing.warpChunksAmount).toFormat(2) }% warp restore
+
);
}
+ let syncStatus = null;
let warpStatus = null;
+ if (syncing.currentBlock && syncing.highestBlock) {
+ syncStatus = (
+
+
+
+ );
+ }
+
if (syncing.blockGap) {
const [first, last] = syncing.blockGap;
warpStatus = (
- , { first.mul(100).div(last).toFormat(2) }% historic
- );
- }
-
- let syncStatus = null;
-
- if (syncing && syncing.currentBlock && syncing.highestBlock) {
- syncStatus = (
- { syncing.currentBlock.toFormat() }/{ syncing.highestBlock.toFormat() } syncing
+
+
+
);
}
diff --git a/js/src/ui/BlockStatus/blockStatus.spec.js b/js/src/ui/BlockStatus/blockStatus.spec.js
new file mode 100644
index 000000000..73358dc90
--- /dev/null
+++ b/js/src/ui/BlockStatus/blockStatus.spec.js
@@ -0,0 +1,94 @@
+// Copyright 2015, 2016 Parity Technologies (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 BigNumber from 'bignumber.js';
+import { shallow } from 'enzyme';
+import React from 'react';
+import sinon from 'sinon';
+
+import BlockStatus from './';
+
+let component;
+
+function createRedux (syncing = false, blockNumber = new BigNumber(123)) {
+ return {
+ dispatch: sinon.stub(),
+ subscribe: sinon.stub(),
+ getState: () => {
+ return {
+ nodeStatus: {
+ blockNumber,
+ syncing
+ }
+ };
+ }
+ };
+}
+
+function render (reduxStore = createRedux(), props) {
+ component = shallow(
+ ,
+ { context: { store: reduxStore } }
+ ).find('BlockStatus').shallow();
+
+ return component;
+}
+
+describe('ui/BlockStatus', () => {
+ it('renders defaults', () => {
+ expect(render()).to.be.ok;
+ });
+
+ it('renders null with no blockNumber', () => {
+ expect(render(createRedux(false, null)).find('div')).to.have.length(0);
+ });
+
+ it('renders only the best block when syncing === false', () => {
+ const messages = render().find('FormattedMessage');
+
+ expect(messages).to.have.length(1);
+ expect(messages).to.have.id('ui.blockStatus.bestBlock');
+ });
+
+ it('renders only the warp restore status when restoring', () => {
+ const messages = render(createRedux({
+ warpChunksAmount: new BigNumber(100),
+ warpChunksProcessed: new BigNumber(5)
+ })).find('FormattedMessage');
+
+ expect(messages).to.have.length(1);
+ expect(messages).to.have.id('ui.blockStatus.warpRestore');
+ });
+
+ it('renders the current/highest when syncing', () => {
+ const messages = render(createRedux({
+ currentBlock: new BigNumber(123),
+ highestBlock: new BigNumber(456)
+ })).find('FormattedMessage');
+
+ expect(messages).to.have.length(1);
+ expect(messages).to.have.id('ui.blockStatus.syncStatus');
+ });
+
+ it('renders warp blockGap when catching up', () => {
+ const messages = render(createRedux({
+ blockGap: [new BigNumber(123), new BigNumber(456)]
+ })).find('FormattedMessage');
+
+ expect(messages).to.have.length(1);
+ expect(messages).to.have.id('ui.blockStatus.warpStatus');
+ });
+});
diff --git a/js/src/ui/Button/button.spec.js b/js/src/ui/Button/button.spec.js
index 101bb19ac..ce0a8bd38 100644
--- a/js/src/ui/Button/button.spec.js
+++ b/js/src/ui/Button/button.spec.js
@@ -19,7 +19,11 @@ import { shallow } from 'enzyme';
import Button from './button';
-function renderShallow (props) {
+function render (props = {}) {
+ if (props && props.label === undefined) {
+ props.label = 'test';
+ }
+
return shallow(
);
@@ -28,11 +32,11 @@ function renderShallow (props) {
describe('ui/Button', () => {
describe('rendering', () => {
it('renders defaults', () => {
- expect(renderShallow()).to.be.ok;
+ expect(render()).to.be.ok;
});
it('renders with the specified className', () => {
- expect(renderShallow({ className: 'testClass' })).to.have.className('testClass');
+ expect(render({ className: 'testClass' })).to.have.className('testClass');
});
});
});
diff --git a/js/src/ui/ConfirmDialog/confirmDialog.js b/js/src/ui/ConfirmDialog/confirmDialog.js
index 103c1562d..5035ced03 100644
--- a/js/src/ui/ConfirmDialog/confirmDialog.js
+++ b/js/src/ui/ConfirmDialog/confirmDialog.js
@@ -15,16 +15,27 @@
// along with Parity. If not, see .
import React, { Component, PropTypes } from 'react';
-import ActionDone from 'material-ui/svg-icons/action/done';
-import ContentClear from 'material-ui/svg-icons/content/clear';
+import { FormattedMessage } from 'react-intl';
import { nodeOrStringProptype } from '~/util/proptypes';
import Button from '../Button';
import Modal from '../Modal';
+import { CancelIcon, CheckIcon } from '../Icons';
import styles from './confirmDialog.css';
+const DEFAULT_NO = (
+
+);
+const DEFAULT_YES = (
+
+);
+
export default class ConfirmDialog extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
@@ -33,10 +44,10 @@ export default class ConfirmDialog extends Component {
iconDeny: PropTypes.node,
labelConfirm: PropTypes.string,
labelDeny: PropTypes.string,
- title: nodeOrStringProptype().isRequired,
- visible: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired,
- onDeny: PropTypes.func.isRequired
+ onDeny: PropTypes.func.isRequired,
+ title: nodeOrStringProptype().isRequired,
+ visible: PropTypes.bool.isRequired
}
render () {
@@ -60,12 +71,12 @@ export default class ConfirmDialog extends Component {
return [
}
+ icon={ iconDeny || }
+ label={ labelDeny || DEFAULT_NO }
onClick={ onDeny } />,
}
+ icon={ iconConfirm || }
+ label={ labelConfirm || DEFAULT_YES }
onClick={ onConfirm } />
];
}
diff --git a/js/src/ui/ConfirmDialog/confirmDialog.spec.js b/js/src/ui/ConfirmDialog/confirmDialog.spec.js
new file mode 100644
index 000000000..6affa4cbf
--- /dev/null
+++ b/js/src/ui/ConfirmDialog/confirmDialog.spec.js
@@ -0,0 +1,157 @@
+// Copyright 2015, 2016 Parity Technologies (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 { shallow } from 'enzyme';
+import React, { PropTypes } from 'react';
+import sinon from 'sinon';
+
+import muiTheme from '../Theme';
+
+import ConfirmDialog from './';
+
+let component;
+let instance;
+let onConfirm;
+let onDeny;
+
+function createRedux () {
+ return {
+ dispatch: sinon.stub(),
+ subscribe: sinon.stub(),
+ getState: () => {
+ return {
+ settings: {
+ backgroundSeed: 'xyz'
+ }
+ };
+ }
+ };
+}
+
+function render (props = {}) {
+ onConfirm = sinon.stub();
+ onDeny = sinon.stub();
+
+ if (props.visible === undefined) {
+ props.visible = true;
+ }
+
+ const baseComponent = shallow(
+
+
+ some test content
+
+
+ );
+
+ instance = baseComponent.instance();
+ component = baseComponent.find('Connect(Modal)').shallow({
+ childContextTypes: {
+ muiTheme: PropTypes.object,
+ store: PropTypes.object
+ },
+ context: {
+ muiTheme,
+ store: createRedux()
+ }
+ });
+
+ return component;
+}
+
+describe('ui/ConfirmDialog', () => {
+ it('renders defaults', () => {
+ expect(render()).to.be.ok;
+ });
+
+ it('renders the body as provided', () => {
+ expect(render().find('div[id="testContent"]').text()).to.equal('some test content');
+ });
+
+ describe('properties', () => {
+ let props;
+
+ beforeEach(() => {
+ props = render().props();
+ });
+
+ it('passes the actions', () => {
+ expect(props.actions).to.deep.equal(instance.renderActions());
+ });
+
+ it('passes title', () => {
+ expect(props.title).to.equal('test title');
+ });
+
+ it('passes visiblity flag', () => {
+ expect(props.visible).to.be.true;
+ });
+ });
+
+ describe('renderActions', () => {
+ describe('defaults', () => {
+ let buttons;
+
+ beforeEach(() => {
+ render();
+ buttons = instance.renderActions();
+ });
+
+ it('renders with supplied onConfim/onDeny callbacks', () => {
+ expect(buttons[0].props.onClick).to.deep.equal(onDeny);
+ expect(buttons[1].props.onClick).to.deep.equal(onConfirm);
+ });
+
+ it('renders default labels', () => {
+ expect(buttons[0].props.label.props.id).to.equal('ui.confirmDialog.no');
+ expect(buttons[1].props.label.props.id).to.equal('ui.confirmDialog.yes');
+ });
+
+ it('renders default icons', () => {
+ expect(buttons[0].props.icon.type.displayName).to.equal('ContentClear');
+ expect(buttons[1].props.icon.type.displayName).to.equal('NavigationCheck');
+ });
+ });
+
+ describe('overrides', () => {
+ let buttons;
+
+ beforeEach(() => {
+ render({
+ labelConfirm: 'labelConfirm',
+ labelDeny: 'labelDeny',
+ iconConfirm: 'iconConfirm',
+ iconDeny: 'iconDeny'
+ });
+ buttons = instance.renderActions();
+ });
+
+ it('renders supplied labels', () => {
+ expect(buttons[0].props.label).to.equal('labelDeny');
+ expect(buttons[1].props.label).to.equal('labelConfirm');
+ });
+
+ it('renders supplied icons', () => {
+ expect(buttons[0].props.icon).to.equal('iconDeny');
+ expect(buttons[1].props.icon).to.equal('iconConfirm');
+ });
+ });
+ });
+});
diff --git a/js/src/ui/Container/container.spec.js b/js/src/ui/Container/container.spec.js
index 5e627999f..105cea164 100644
--- a/js/src/ui/Container/container.spec.js
+++ b/js/src/ui/Container/container.spec.js
@@ -19,7 +19,7 @@ import { shallow } from 'enzyme';
import Container from './container';
-function renderShallow (props) {
+function render (props) {
return shallow(
);
@@ -28,11 +28,24 @@ function renderShallow (props) {
describe('ui/Container', () => {
describe('rendering', () => {
it('renders defaults', () => {
- expect(renderShallow()).to.be.ok;
+ expect(render()).to.be.ok;
});
it('renders with the specified className', () => {
- expect(renderShallow({ className: 'testClass' })).to.have.className('testClass');
+ expect(render({ className: 'testClass' })).to.have.className('testClass');
+ });
+ });
+
+ describe('sections', () => {
+ it('renders the Card', () => {
+ expect(render().find('Card')).to.have.length(1);
+ });
+
+ it('renders the Title', () => {
+ const title = render({ title: 'title' }).find('Title');
+
+ expect(title).to.have.length(1);
+ expect(title.props().title).to.equal('title');
});
});
});
diff --git a/js/src/ui/GasPriceEditor/gasPriceEditor.spec.js b/js/src/ui/GasPriceEditor/gasPriceEditor.spec.js
index 336ec5c7e..3a414f90e 100644
--- a/js/src/ui/GasPriceEditor/gasPriceEditor.spec.js
+++ b/js/src/ui/GasPriceEditor/gasPriceEditor.spec.js
@@ -29,6 +29,7 @@ const api = {
const store = {
estimated: '123',
+ histogram: {},
priceDefault: '456',
totalValue: '789',
setGas: sinon.stub(),
diff --git a/js/src/ui/IdentityIcon/identityIcon.js b/js/src/ui/IdentityIcon/identityIcon.js
index 5ab1651ec..d4d65241a 100644
--- a/js/src/ui/IdentityIcon/identityIcon.js
+++ b/js/src/ui/IdentityIcon/identityIcon.js
@@ -34,8 +34,8 @@ class IdentityIcon extends Component {
button: PropTypes.bool,
center: PropTypes.bool,
className: PropTypes.string,
- inline: PropTypes.bool,
images: PropTypes.object.isRequired,
+ inline: PropTypes.bool,
padded: PropTypes.bool,
tiny: PropTypes.bool
}
diff --git a/js/src/ui/IdentityIcon/identityIcon.spec.js b/js/src/ui/IdentityIcon/identityIcon.spec.js
new file mode 100644
index 000000000..759907deb
--- /dev/null
+++ b/js/src/ui/IdentityIcon/identityIcon.spec.js
@@ -0,0 +1,120 @@
+// Copyright 2015, 2016 Parity Technologies (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 { mount } from 'enzyme';
+import React, { PropTypes } from 'react';
+import sinon from 'sinon';
+
+import muiTheme from '../Theme';
+
+import IdentityIcon from './';
+
+const ADDRESS0 = '0x0000000000000000000000000000000000000000';
+const ADDRESS1 = '0x0123456789012345678901234567890123456789';
+const ADDRESS2 = '0x9876543210987654321098765432109876543210';
+
+let component;
+
+function createApi () {
+ return {
+ dappsUrl: 'dappsUrl/'
+ };
+}
+
+function createRedux () {
+ return {
+ dispatch: sinon.stub(),
+ subscribe: sinon.stub(),
+ getState: () => {
+ return {
+ images: {
+ [ADDRESS2]: 'reduxImage'
+ }
+ };
+ }
+ };
+}
+
+function render (props = {}) {
+ if (props && props.address === undefined) {
+ props.address = ADDRESS1;
+ }
+
+ component = mount(
+ ,
+ {
+ childContextTypes: {
+ api: PropTypes.object,
+ muiTheme: PropTypes.object
+ },
+ context: {
+ api: createApi(),
+ muiTheme,
+ store: createRedux()
+ }
+ }
+ );
+
+ return component;
+}
+
+describe('ui/IdentityIcon', () => {
+ it('renders defaults', () => {
+ expect(render()).to.be.ok;
+ });
+
+ describe('images', () => {
+ it('renders an
with address specified', () => {
+ const img = render().find('img');
+
+ expect(img).to.have.length(1);
+ expect(img.props().src).to.equal('test-createIdentityImg');
+ });
+
+ it('renders an
with redux source when available', () => {
+ const img = render({ address: ADDRESS2 }).find('img');
+
+ expect(img).to.have.length(1);
+ expect(img.props().src).to.equal('dappsUrl/reduxImage');
+ });
+
+ it('renders an with no address specified', () => {
+ expect(render({ address: null }).find('ActionCode')).to.have.length(1);
+ });
+
+ it('renders an with 0x00..00 address specified', () => {
+ expect(render({ address: ADDRESS0 }).find('ContentClear')).to.have.length(1);
+ });
+ });
+
+ describe('sizes', () => {
+ it('renders 56px by default', () => {
+ expect(render().find('img').props().width).to.equal('56px');
+ });
+
+ it('renders 16px for tiny', () => {
+ expect(render({ tiny: true }).find('img').props().width).to.equal('16px');
+ });
+
+ it('renders 24px for button', () => {
+ expect(render({ button: true }).find('img').props().width).to.equal('24px');
+ });
+
+ it('renders 32px for inline', () => {
+ expect(render({ inline: true }).find('img').props().width).to.equal('32px');
+ });
+ });
+});
diff --git a/js/src/ui/IdentityName/identityName.js b/js/src/ui/IdentityName/identityName.js
index 45e864a75..980f42638 100644
--- a/js/src/ui/IdentityName/identityName.js
+++ b/js/src/ui/IdentityName/identityName.js
@@ -15,13 +15,23 @@
// along with Parity. If not, see .
import React, { Component, PropTypes } from 'react';
+import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { isNullAddress } from '~/util/validation';
import ShortenedHash from '../ShortenedHash';
-const defaultName = 'UNNAMED';
+const defaultName = (
+
+);
+const defaultNameNull = (
+
+);
class IdentityName extends Component {
static propTypes = {
@@ -43,7 +53,7 @@ class IdentityName extends Component {
return null;
}
- const nullName = isNullAddress(address) ? 'null' : null;
+ const nullName = isNullAddress(address) ? defaultNameNull : null;
const addressFallback = nullName || (shorten ? () : address);
const fallback = unknown ? defaultName : addressFallback;
const isUuid = account && account.name === account.uuid;
diff --git a/js/src/ui/IdentityName/identityName.spec.js b/js/src/ui/IdentityName/identityName.spec.js
index bb0b55e46..12bd07363 100644
--- a/js/src/ui/IdentityName/identityName.spec.js
+++ b/js/src/ui/IdentityName/identityName.spec.js
@@ -14,8 +14,10 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-import React from 'react';
import { mount } from 'enzyme';
+import React from 'react';
+import { IntlProvider } from 'react-intl';
+
import sinon from 'sinon';
import IdentityName from './identityName';
@@ -44,9 +46,11 @@ const STORE = {
function render (props) {
return mount(
-
+
+
+
);
}
@@ -74,7 +78,7 @@ describe('ui/IdentityName', () => {
});
it('renders 0x000...000 as null', () => {
- expect(render({ address: ADDR_NULL }).text()).to.equal('null');
+ expect(render({ address: ADDR_NULL }).text()).to.equal('NULL');
});
});
});
diff --git a/js/src/ui/TxHash/txHash.js b/js/src/ui/TxHash/txHash.js
index 09905d594..df5e9342e 100644
--- a/js/src/ui/TxHash/txHash.js
+++ b/js/src/ui/TxHash/txHash.js
@@ -16,6 +16,7 @@
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
+import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { LinearProgress } from 'material-ui';
@@ -33,8 +34,8 @@ class TxHash extends Component {
static propTypes = {
hash: PropTypes.string.isRequired,
isTest: PropTypes.bool,
- summary: PropTypes.bool,
- maxConfirmations: PropTypes.number
+ maxConfirmations: PropTypes.number,
+ summary: PropTypes.bool
}
static defaultProps = {
@@ -43,14 +44,14 @@ class TxHash extends Component {
state = {
blockNumber: new BigNumber(0),
- transaction: null,
- subscriptionId: null
+ subscriptionId: null,
+ transaction: null
}
componentDidMount () {
const { api } = this.context;
- api.subscribe('eth_blockNumber', this.onBlockNumber).then((subscriptionId) => {
+ return api.subscribe('eth_blockNumber', this.onBlockNumber).then((subscriptionId) => {
this.setState({ subscriptionId });
});
}
@@ -59,28 +60,28 @@ class TxHash extends Component {
const { api } = this.context;
const { subscriptionId } = this.state;
- api.unsubscribe(subscriptionId);
+ return api.unsubscribe(subscriptionId);
}
render () {
const { hash, isTest, summary } = this.props;
- const link = (
+ const hashLink = (
);
- let header = (
- The transaction has been posted to the network, with a hash of { link }.
- );
- if (summary) {
- header = ({ link }
);
- }
-
return (
- { header }
+
{
+ summary
+ ? hashLink
+ :
+ }
{ this.renderConfirmations() }
);
@@ -98,20 +99,22 @@ class TxHash extends Component {
color='white'
mode='indeterminate'
/>
- waiting for confirmations
+
+
+
);
}
const confirmations = blockNumber.minus(transaction.blockNumber).plus(1);
const value = Math.min(confirmations.toNumber(), maxConfirmations);
- let count;
- if (confirmations.gt(maxConfirmations)) {
- count = confirmations.toFormat(0);
- } else {
- count = confirmations.toFormat(0) + `/${maxConfirmations}`;
+
+ let count = confirmations.toFormat(0);
+ if (confirmations.lte(maxConfirmations)) {
+ count = `${count}/${maxConfirmations}`;
}
- const unit = value === 1 ? 'confirmation' : 'confirmations';
return (
@@ -121,10 +124,17 @@ class TxHash extends Component {
max={ maxConfirmations }
value={ value }
color='white'
- mode='determinate'
- />
+ mode='determinate' />
-
{ count } { unit }
+
+
+
);
@@ -138,15 +148,17 @@ class TxHash extends Component {
return;
}
- this.setState({ blockNumber });
-
- api.eth
+ return api.eth
.getTransactionReceipt(hash)
.then((transaction) => {
- this.setState({ transaction });
+ this.setState({
+ blockNumber,
+ transaction
+ });
})
.catch((error) => {
console.warn('onBlockNumber', error);
+ this.setState({ blockNumber });
});
}
}
diff --git a/js/src/ui/TxHash/txHash.spec.js b/js/src/ui/TxHash/txHash.spec.js
new file mode 100644
index 000000000..7f47363ee
--- /dev/null
+++ b/js/src/ui/TxHash/txHash.spec.js
@@ -0,0 +1,132 @@
+// Copyright 2015, 2016 Parity Technologies (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 BigNumber from 'bignumber.js';
+import { shallow } from 'enzyme';
+import React from 'react';
+import sinon from 'sinon';
+
+import TxHash from './';
+
+const TXHASH = '0xabcdef123454321abcdef';
+
+let api;
+let blockNumber;
+let callback;
+let component;
+let instance;
+
+function createApi () {
+ blockNumber = new BigNumber(100);
+ api = {
+ eth: {
+ getTransactionReceipt: (hash) => {
+ return Promise.resolve({
+ blockNumber: new BigNumber(100),
+ hash
+ });
+ }
+ },
+ nextBlock: (increment = 1) => {
+ blockNumber = blockNumber.plus(increment);
+ return callback(null, blockNumber);
+ },
+ subscribe: (type, _callback) => {
+ callback = _callback;
+ return callback(null, blockNumber).then(() => {
+ return Promise.resolve(1);
+ });
+ },
+ unsubscribe: sinon.stub().resolves(true)
+ };
+
+ return api;
+}
+
+function createRedux () {
+ return {
+ dispatch: sinon.stub(),
+ subscribe: sinon.stub(),
+ getState: () => {
+ return {
+ nodeStatus: { isTest: true }
+ };
+ }
+ };
+}
+
+function render (props) {
+ const baseComponent = shallow(
+ ,
+ { context: { store: createRedux() } }
+ );
+ component = baseComponent.find('TxHash').shallow({ context: { api: createApi() } });
+ instance = component.instance();
+
+ return component;
+}
+
+describe('ui/TxHash', () => {
+ beforeEach(() => {
+ render();
+ });
+
+ it('renders defaults', () => {
+ expect(component).to.be.ok;
+ });
+
+ it('renders the summary', () => {
+ expect(component.find('p').find('FormattedMessage').props().id).to.equal('ui.txHash.posted');
+ });
+
+ describe('renderConfirmations', () => {
+ describe('with no transaction retrieved', () => {
+ let child;
+
+ beforeEach(() => {
+ child = shallow(instance.renderConfirmations());
+ });
+
+ it('renders indeterminate progressbar', () => {
+ expect(child.find('LinearProgress[mode="indeterminate"]')).to.have.length(1);
+ });
+
+ it('renders waiting text', () => {
+ expect(child.find('FormattedMessage').props().id).to.equal('ui.txHash.waiting');
+ });
+ });
+
+ describe('with transaction retrieved', () => {
+ let child;
+
+ beforeEach(() => {
+ return instance.componentDidMount().then(() => {
+ child = shallow(instance.renderConfirmations());
+ });
+ });
+
+ it('renders determinate progressbar', () => {
+ expect(child.find('LinearProgress[mode="determinate"]')).to.have.length(1);
+ });
+
+ it('renders confirmation text', () => {
+ expect(child.find('FormattedMessage').props().id).to.equal('ui.txHash.confirmations');
+ });
+ });
+ });
+});
diff --git a/js/src/ui/TxList/TxRow/txRow.spec.js b/js/src/ui/TxList/TxRow/txRow.spec.js
index ddee9024c..030ff4432 100644
--- a/js/src/ui/TxList/TxRow/txRow.spec.js
+++ b/js/src/ui/TxList/TxRow/txRow.spec.js
@@ -25,7 +25,7 @@ import TxRow from './txRow';
const api = new Api({ execute: sinon.stub() });
-function renderShallow (props) {
+function render (props) {
return shallow(
,
@@ -33,7 +33,7 @@ function renderShallow (props) {
);
}
-describe('ui/TxRow', () => {
+describe('ui/TxList/TxRow', () => {
describe('rendering', () => {
it('renders defaults', () => {
const block = {
@@ -45,7 +45,7 @@ describe('ui/TxRow', () => {
value: new BigNumber(1)
};
- expect(renderShallow({ block, tx })).to.be.ok;
+ expect(render({ address: '0x123', block, isTest: true, tx })).to.be.ok;
});
});
});
diff --git a/js/src/ui/TxList/txList.spec.js b/js/src/ui/TxList/txList.spec.js
index 88012888c..11367fbce 100644
--- a/js/src/ui/TxList/txList.spec.js
+++ b/js/src/ui/TxList/txList.spec.js
@@ -36,7 +36,7 @@ const STORE = {
}
};
-function renderShallow (props) {
+function render (props) {
return shallow(
{
describe('rendering', () => {
it('renders defaults', () => {
- expect(renderShallow()).to.be.ok;
+ expect(render({ address: '0x123', hashes: [] })).to.be.ok;
});
});
});
From d16ab5eac50963c0ef9fb5bfa9e33a75f68cf914 Mon Sep 17 00:00:00 2001
From: Nicolas Gotchac
Date: Thu, 5 Jan 2017 12:06:58 +0100
Subject: [PATCH 20/22] Show contract parameters in MethodDecoding (#4024)
* Add decoding of Inner Contract Deployment params #3715
* Fixed TypedInput when formatted value
* Fix TypedInput
* PR Grumble
* Add test to `Param.toParams`
---
js/src/abi/spec/param.js | 8 +-
js/src/abi/spec/param.spec.js | 9 ++
js/src/api/util/decode.js | 12 +-
js/src/ui/Form/AddressSelect/addressSelect.js | 6 +-
js/src/ui/Form/InputAddress/inputAddress.js | 11 +-
.../InputAddressSelect/inputAddressSelect.js | 4 +-
js/src/ui/Form/TypedInput/typedInput.js | 45 +++++--
js/src/ui/MethodDecoding/methodDecoding.js | 56 ++++----
.../ui/MethodDecoding/methodDecodingStore.js | 120 +++++++++++++++---
9 files changed, 207 insertions(+), 64 deletions(-)
diff --git a/js/src/abi/spec/param.js b/js/src/abi/spec/param.js
index 88696ceed..d7a85c009 100644
--- a/js/src/abi/spec/param.js
+++ b/js/src/abi/spec/param.js
@@ -31,6 +31,12 @@ export default class Param {
}
static toParams (params) {
- return params.map((param) => new Param(param.name, param.type));
+ return params.map((param) => {
+ if (param instanceof Param) {
+ return param;
+ }
+
+ return new Param(param.name, param.type);
+ });
}
}
diff --git a/js/src/abi/spec/param.spec.js b/js/src/abi/spec/param.spec.js
index 9957df909..c1dcddeb5 100644
--- a/js/src/abi/spec/param.spec.js
+++ b/js/src/abi/spec/param.spec.js
@@ -34,5 +34,14 @@ describe('abi/spec/Param', () => {
expect(params[0].name).to.equal('foo');
expect(params[0].kind.type).to.equal('uint');
});
+
+ it('converts only if needed', () => {
+ const _params = Param.toParams([{ name: 'foo', type: 'uint' }]);
+ const params = Param.toParams(_params);
+
+ expect(params.length).to.equal(1);
+ expect(params[0].name).to.equal('foo');
+ expect(params[0].kind.type).to.equal('uint');
+ });
});
});
diff --git a/js/src/api/util/decode.js b/js/src/api/util/decode.js
index 0e0164bec..d0cea05c1 100644
--- a/js/src/api/util/decode.js
+++ b/js/src/api/util/decode.js
@@ -26,7 +26,9 @@ export function decodeCallData (data) {
if (data.substr(0, 2) === '0x') {
return decodeCallData(data.slice(2));
- } else if (data.length < 8) {
+ }
+
+ if (data.length < 8) {
throw new Error('Input to decodeCallData should be method signature + data');
}
@@ -42,10 +44,14 @@ export function decodeCallData (data) {
export function decodeMethodInput (methodAbi, paramdata) {
if (!methodAbi) {
throw new Error('decodeMethodInput should receive valid method-specific ABI');
- } else if (paramdata && paramdata.length) {
+ }
+
+ if (paramdata && paramdata.length) {
if (!isHex(paramdata)) {
throw new Error('Input to decodeMethodInput should be a hex value');
- } else if (paramdata.substr(0, 2) === '0x') {
+ }
+
+ if (paramdata.substr(0, 2) === '0x') {
return decodeMethodInput(methodAbi, paramdata.slice(2));
}
}
diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js
index ba48b6489..692ff4285 100644
--- a/js/src/ui/Form/AddressSelect/addressSelect.js
+++ b/js/src/ui/Form/AddressSelect/addressSelect.js
@@ -60,6 +60,7 @@ class AddressSelect extends Component {
// Optional props
allowCopy: PropTypes.bool,
allowInput: PropTypes.bool,
+ className: PropTypes.string,
disabled: PropTypes.bool,
error: nodeOrStringProptype(),
hint: nodeOrStringProptype(),
@@ -123,13 +124,14 @@ class AddressSelect extends Component {
renderInput () {
const { focused } = this.state;
- const { accountsInfo, allowCopy, disabled, error, hint, label, readOnly, value } = this.props;
+ const { accountsInfo, allowCopy, className, disabled, error, hint, label, readOnly, value } = this.props;
const input = (
+ }
+ { ...props }
+ />
{ icon }
);
diff --git a/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js b/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js
index 60a0f8d1b..f5b218694 100644
--- a/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js
+++ b/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js
@@ -27,6 +27,7 @@ class InputAddressSelect extends Component {
contracts: PropTypes.object.isRequired,
allowCopy: PropTypes.bool,
+ className: PropTypes.string,
error: PropTypes.string,
hint: PropTypes.string,
label: PropTypes.string,
@@ -36,13 +37,14 @@ class InputAddressSelect extends Component {
};
render () {
- const { accounts, allowCopy, contacts, contracts, label, hint, error, value, onChange, readOnly } = this.props;
+ const { accounts, allowCopy, className, contacts, contracts, label, hint, error, value, onChange, readOnly } = this.props;
return (
.
-import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import CircularProgress from 'material-ui/CircularProgress';
-import { Input, InputAddress } from '../Form';
+import { TypedInput, InputAddress } from '../Form';
import MethodDecodingStore from './methodDecodingStore';
import styles from './methodDecoding.css';
@@ -245,6 +244,7 @@ class MethodDecoding extends Component {
renderDeploy () {
const { historic, transaction } = this.props;
+ const { methodInputs } = this.state;
if (!historic) {
return (
@@ -261,6 +261,14 @@ class MethodDecoding extends Component {
{ this.renderAddressName(transaction.creates, false) }
+
+
+ { methodInputs && methodInputs.length ? 'with the following parameters:' : ''}
+
+
+
+ { this.renderInputs() }
+
);
}
@@ -364,39 +372,31 @@ class MethodDecoding extends Component {
renderInputs () {
const { methodInputs } = this.state;
- return methodInputs.map((input, index) => {
- switch (input.type) {
- case 'address':
- return (
-
- );
+ if (!methodInputs || methodInputs.length === 0) {
+ return null;
+ }
- default:
- return (
-
- );
- }
+ const inputs = methodInputs.map((input, index) => {
+ return (
+
+ );
});
+
+ return inputs;
}
renderValue (value) {
const { api } = this.context;
- if (api.util.isInstanceOf(value, BigNumber)) {
- return value.toFormat(0);
- } else if (api.util.isArray(value)) {
+ if (api.util.isArray(value)) {
return api.util.bytesToHex(value);
}
diff --git a/js/src/ui/MethodDecoding/methodDecodingStore.js b/js/src/ui/MethodDecoding/methodDecodingStore.js
index 5d518d3a9..b31412c21 100644
--- a/js/src/ui/MethodDecoding/methodDecodingStore.js
+++ b/js/src/ui/MethodDecoding/methodDecodingStore.js
@@ -18,6 +18,8 @@ import Contracts from '~/contracts';
import Abi from '~/abi';
import * as abis from '~/contracts/abi';
+import { decodeMethodInput } from '~/api/util/decode';
+
const CONTRACT_CREATE = '0x60606040';
let instance = null;
@@ -26,6 +28,8 @@ export default class MethodDecodingStore {
api = null;
+ _bytecodes = {};
+ _contractsAbi = {};
_isContract = {};
_methods = {};
@@ -46,12 +50,17 @@ export default class MethodDecodingStore {
if (!contract || !contract.meta || !contract.meta.abi) {
return;
}
- this.loadFromAbi(contract.meta.abi);
+ this.loadFromAbi(contract.meta.abi, contract.address);
});
}
- loadFromAbi (_abi) {
+ loadFromAbi (_abi, contractAddress) {
const abi = new Abi(_abi);
+
+ if (contractAddress && abi) {
+ this._contractsAbi[contractAddress] = abi;
+ }
+
abi
.functions
.map((f) => ({ sign: f.signature, abi: f.abi }))
@@ -111,6 +120,7 @@ export default class MethodDecodingStore {
const contractAddress = isReceived ? transaction.from : transaction.to;
const input = transaction.input || transaction.data;
+ result.input = input;
result.received = isReceived;
// No input, should be a ETH transfer
@@ -118,17 +128,20 @@ export default class MethodDecodingStore {
return Promise.resolve(result);
}
- try {
- const { signature } = this.api.util.decodeCallData(input);
+ let signature;
- if (signature === CONTRACT_CREATE || transaction.creates) {
- result.contract = true;
- return Promise.resolve({ ...result, deploy: true });
- }
+ try {
+ const decodeCallDataResult = this.api.util.decodeCallData(input);
+ signature = decodeCallDataResult.signature;
} catch (e) {}
+ // Contract deployment
+ if (!signature || signature === CONTRACT_CREATE || transaction.creates) {
+ return this.decodeContractCreation(result, contractAddress || transaction.creates);
+ }
+
return this
- .isContract(contractAddress || transaction.creates)
+ .isContract(contractAddress)
.then((isContract) => {
result.contract = isContract;
@@ -140,11 +153,6 @@ export default class MethodDecodingStore {
result.signature = signature;
result.params = paramdata;
- // Contract deployment
- if (!signature) {
- return Promise.resolve({ ...result, deploy: true });
- }
-
return this
.fetchMethodAbi(signature)
.then((abi) => {
@@ -173,6 +181,68 @@ export default class MethodDecodingStore {
});
}
+ decodeContractCreation (data, contractAddress) {
+ const result = {
+ ...data,
+ contract: true,
+ deploy: true
+ };
+
+ const { input } = data;
+ const abi = this._contractsAbi[contractAddress];
+
+ if (!input || !abi || !abi.constructors || abi.constructors.length === 0) {
+ return Promise.resolve(result);
+ }
+
+ const constructorAbi = abi.constructors[0];
+
+ const rawInput = /^(?:0x)?(.*)$/.exec(input)[1];
+
+ return this
+ .getCode(contractAddress)
+ .then((code) => {
+ if (!code || /^(0x)0*?$/.test(code)) {
+ return result;
+ }
+
+ const rawCode = /^(?:0x)?(.*)$/.exec(code)[1];
+ const codeOffset = rawInput.indexOf(rawCode);
+
+ if (codeOffset === -1) {
+ return result;
+ }
+
+ // Params are the last bytes of the transaction Input
+ // (minus the bytecode). It seems that they are repeated
+ // twice
+ const params = rawInput.slice(codeOffset + rawCode.length);
+ const paramsBis = params.slice(params.length / 2);
+
+ let decodedInputs;
+
+ try {
+ decodedInputs = decodeMethodInput(constructorAbi, params);
+ } catch (e) {}
+
+ try {
+ if (!decodedInputs) {
+ decodedInputs = decodeMethodInput(constructorAbi, paramsBis);
+ }
+ } catch (e) {}
+
+ if (decodedInputs && decodedInputs.length > 0) {
+ result.inputs = decodedInputs
+ .map((value, index) => {
+ const type = constructorAbi.inputs[index].kind.type;
+ return { type, value };
+ });
+ }
+
+ return result;
+ });
+ }
+
fetchMethodAbi (signature) {
if (this._methods[signature] !== undefined) {
return Promise.resolve(this._methods[signature]);
@@ -209,7 +279,7 @@ export default class MethodDecodingStore {
return Promise.resolve(this._isContract[contractAddress]);
}
- this._isContract[contractAddress] = this.api.eth
+ this._isContract[contractAddress] = this
.getCode(contractAddress)
.then((bytecode) => {
// Is a contract if the address contains *valid* bytecode
@@ -222,4 +292,24 @@ export default class MethodDecodingStore {
return Promise.resolve(this._isContract[contractAddress]);
}
+ getCode (contractAddress) {
+ // If zero address, resolve to '0x'
+ if (!contractAddress || /^(0x)?0*$/.test(contractAddress)) {
+ return Promise.resolve('0x');
+ }
+
+ if (this._bytecodes[contractAddress]) {
+ return Promise.resolve(this._bytecodes[contractAddress]);
+ }
+
+ this._bytecodes[contractAddress] = this.api.eth
+ .getCode(contractAddress)
+ .then((bytecode) => {
+ this._bytecodes[contractAddress] = bytecode;
+ return this._bytecodes[contractAddress];
+ });
+
+ return Promise.resolve(this._bytecodes[contractAddress]);
+ }
+
}
From 1ef67f68ed28dde17eb651676fb6d5080467e11b Mon Sep 17 00:00:00 2001
From: Jaco Greeff
Date: Thu, 5 Jan 2017 12:07:10 +0100
Subject: [PATCH 21/22] Starting on homestead shows reload snackbar (#4043)
* Fix issue where starting on homestead showed reload
* Align snackbar timing with errors (60s)
---
js/src/redux/providers/chainMiddleware.js | 7 +-
.../redux/providers/chainMiddleware.spec.js | 86 +++++++++++++++++++
js/src/redux/providers/statusReducer.js | 8 +-
3 files changed, 97 insertions(+), 4 deletions(-)
create mode 100644 js/src/redux/providers/chainMiddleware.spec.js
diff --git a/js/src/redux/providers/chainMiddleware.js b/js/src/redux/providers/chainMiddleware.js
index 82281f3b8..77c757da6 100644
--- a/js/src/redux/providers/chainMiddleware.js
+++ b/js/src/redux/providers/chainMiddleware.js
@@ -15,6 +15,7 @@
// along with Parity. If not, see .
import { showSnackbar } from './snackbarActions';
+import { DEFAULT_NETCHAIN } from './statusReducer';
export default class ChainMiddleware {
toMiddleware () {
@@ -23,11 +24,11 @@ export default class ChainMiddleware {
const { collection } = action;
if (collection && collection.netChain) {
- const chain = collection.netChain;
+ const newChain = collection.netChain;
const { nodeStatus } = store.getState();
- if (chain !== nodeStatus.netChain) {
- store.dispatch(showSnackbar(`Switched to ${chain}. Please reload the page.`, 5000));
+ if (newChain !== nodeStatus.netChain && nodeStatus.netChain !== DEFAULT_NETCHAIN) {
+ store.dispatch(showSnackbar(`Switched to ${newChain}. Please reload the page.`, 60000));
}
}
}
diff --git a/js/src/redux/providers/chainMiddleware.spec.js b/js/src/redux/providers/chainMiddleware.spec.js
new file mode 100644
index 000000000..ed2d5eca6
--- /dev/null
+++ b/js/src/redux/providers/chainMiddleware.spec.js
@@ -0,0 +1,86 @@
+// Copyright 2015, 2016 Parity Technologies (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 sinon from 'sinon';
+
+import { initialState as defaultNodeStatusState } from './statusReducer';
+import ChainMiddleware from './chainMiddleware';
+
+let middleware;
+let next;
+let store;
+
+function createMiddleware (collection = {}) {
+ middleware = new ChainMiddleware().toMiddleware();
+ next = sinon.stub();
+ store = {
+ dispatch: sinon.stub(),
+ getState: () => {
+ return {
+ nodeStatus: Object.assign({}, defaultNodeStatusState, collection)
+ };
+ }
+ };
+
+ return middleware;
+}
+
+function callMiddleware (action) {
+ return middleware(store)(next)(action);
+}
+
+describe('reduxs/providers/ChainMiddleware', () => {
+ describe('next action', () => {
+ beforeEach(() => {
+ createMiddleware();
+ });
+
+ it('calls next with matching actiontypes', () => {
+ callMiddleware({ type: 'statusCollection' });
+
+ expect(next).to.have.been.calledWithMatch({ type: 'statusCollection' });
+ });
+
+ it('calls next with non-matching actiontypes', () => {
+ callMiddleware({ type: 'nonMatchingType' });
+
+ expect(next).to.have.been.calledWithMatch({ type: 'nonMatchingType' });
+ });
+ });
+
+ describe('chain switching', () => {
+ it('does not dispatch when moving from the initial/unknown chain', () => {
+ createMiddleware();
+ callMiddleware({ type: 'statusCollection', collection: { netChain: 'homestead' } });
+
+ expect(store.dispatch).not.to.have.been.called;
+ });
+
+ it('does not dispatch when moving to the same chain', () => {
+ createMiddleware({ netChain: 'homestead' });
+ callMiddleware({ type: 'statusCollection', collection: { netChain: 'homestead' } });
+
+ expect(store.dispatch).not.to.have.been.called;
+ });
+
+ it('does dispatch when moving between chains', () => {
+ createMiddleware({ netChain: 'homestead' });
+ callMiddleware({ type: 'statusCollection', collection: { netChain: 'ropsten' } });
+
+ expect(store.dispatch).to.have.been.called;
+ });
+ });
+});
diff --git a/js/src/redux/providers/statusReducer.js b/js/src/redux/providers/statusReducer.js
index 17186b012..4bef27b1b 100644
--- a/js/src/redux/providers/statusReducer.js
+++ b/js/src/redux/providers/statusReducer.js
@@ -17,6 +17,7 @@
import BigNumber from 'bignumber.js';
import { handleActions } from 'redux-actions';
+const DEFAULT_NETCHAIN = '(unknown)';
const initialState = {
blockNumber: new BigNumber(0),
blockTimestamp: new Date(),
@@ -32,7 +33,7 @@ const initialState = {
gasLimit: new BigNumber(0),
hashrate: new BigNumber(0),
minGasPrice: new BigNumber(0),
- netChain: 'ropsten',
+ netChain: DEFAULT_NETCHAIN,
netPeers: {
active: new BigNumber(0),
connected: new BigNumber(0),
@@ -82,3 +83,8 @@ export default handleActions({
return Object.assign({}, state, { refreshStatus });
}
}, initialState);
+
+export {
+ DEFAULT_NETCHAIN,
+ initialState
+};
From e7e561024a4170d4c9badf040127cf1009cfc1e5 Mon Sep 17 00:00:00 2001
From: GitLab Build Bot
Date: Thu, 5 Jan 2017 11:15:57 +0000
Subject: [PATCH 22/22] [ci skip] js-precompiled 20170105-111302
---
Cargo.lock | 2 +-
js/package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 2fee4b290..17928f75b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1500,7 +1500,7 @@ dependencies = [
[[package]]
name = "parity-ui-precompiled"
version = "1.4.0"
-source = "git+https://github.com/ethcore/js-precompiled.git#9fc9d894356b135c66cc99bf66ec2a713b5f0a8c"
+source = "git+https://github.com/ethcore/js-precompiled.git#257b3ce8aaa6797507592200dc78b29b8a305c3f"
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 29bb70791..79e8d473e 100644
--- a/js/package.json
+++ b/js/package.json
@@ -1,6 +1,6 @@
{
"name": "parity.js",
- "version": "0.2.167",
+ "version": "0.2.168",
"main": "release/index.js",
"jsnext:main": "src/index.js",
"author": "Parity Team ",