diff --git a/Cargo.lock b/Cargo.lock
index 08edcc166..776c26c42 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1503,7 +1503,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#257b3ce8aaa6797507592200dc78b29b8a305c3f"
dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
diff --git a/ethcore/src/account_provider/mod.rs b/ethcore/src/account_provider/mod.rs
index 5b56c697c..2f810d6be 100644
--- a/ethcore/src/account_provider/mod.rs
+++ b/ethcore/src/account_provider/mod.rs
@@ -150,6 +150,11 @@ impl AccountProvider {
Ok(Address::from(address).into())
}
+ /// Checks whether an account with a given address is present.
+ pub fn has_account(&self, address: Address) -> Result {
+ Ok(self.accounts()?.iter().any(|&a| a == address))
+ }
+
/// Returns addresses of all accounts.
pub fn accounts(&self) -> Result, Error> {
let accounts = self.sstore.accounts()?;
diff --git a/ethcore/src/snapshot/account.rs b/ethcore/src/snapshot/account.rs
index 0dfb72955..9d9aa0b9e 100644
--- a/ethcore/src/snapshot/account.rs
+++ b/ethcore/src/snapshot/account.rs
@@ -17,16 +17,17 @@
//! Account state encoding and decoding
use account_db::{AccountDB, AccountDBMut};
+use basic_account::BasicAccount;
use snapshot::Error;
use util::{U256, FixedHash, H256, Bytes, HashDB, SHA3_EMPTY, SHA3_NULL_RLP};
use util::trie::{TrieDB, Trie};
-use rlp::{Rlp, RlpStream, Stream, UntrustedRlp, View};
+use rlp::{RlpStream, Stream, UntrustedRlp, View};
use std::collections::HashSet;
// An empty account -- these are replaced with RLP null data for a space optimization.
-const ACC_EMPTY: Account = Account {
+const ACC_EMPTY: BasicAccount = BasicAccount {
nonce: U256([0, 0, 0, 0]),
balance: U256([0, 0, 0, 0]),
storage_root: SHA3_NULL_RLP,
@@ -59,165 +60,121 @@ impl CodeState {
}
}
-// An alternate account structure from ::account::Account.
-#[derive(PartialEq, Clone, Debug)]
-pub struct Account {
- nonce: U256,
- balance: U256,
- storage_root: H256,
- code_hash: H256,
+// walk the account's storage trie, returning an RLP item containing the
+// account properties and the storage.
+pub fn to_fat_rlp(acc: &BasicAccount, acct_db: &AccountDB, used_code: &mut HashSet) -> Result {
+ if acc == &ACC_EMPTY {
+ return Ok(::rlp::NULL_RLP.to_vec());
+ }
+
+ let db = TrieDB::new(acct_db, &acc.storage_root)?;
+
+ let mut pairs = Vec::new();
+
+ for item in db.iter()? {
+ let (k, v) = item?;
+ pairs.push((k, v));
+ }
+
+ let mut stream = RlpStream::new_list(pairs.len());
+
+ for (k, v) in pairs {
+ stream.begin_list(2).append(&k).append(&&*v);
+ }
+
+ let pairs_rlp = stream.out();
+
+ let mut account_stream = RlpStream::new_list(5);
+ account_stream.append(&acc.nonce)
+ .append(&acc.balance);
+
+ // [has_code, code_hash].
+ if acc.code_hash == SHA3_EMPTY {
+ account_stream.append(&CodeState::Empty.raw()).append_empty_data();
+ } else if used_code.contains(&acc.code_hash) {
+ account_stream.append(&CodeState::Hash.raw()).append(&acc.code_hash);
+ } else {
+ match acct_db.get(&acc.code_hash) {
+ Some(c) => {
+ used_code.insert(acc.code_hash.clone());
+ account_stream.append(&CodeState::Inline.raw()).append(&&*c);
+ }
+ None => {
+ warn!("code lookup failed during snapshot");
+ account_stream.append(&false).append_empty_data();
+ }
+ }
+ }
+
+ account_stream.append_raw(&pairs_rlp, 1);
+
+ Ok(account_stream.out())
}
-impl Account {
- // decode the account from rlp.
- pub fn from_thin_rlp(rlp: &[u8]) -> Self {
- let r: Rlp = Rlp::new(rlp);
+// decode a fat rlp, and rebuild the storage trie as we go.
+// returns the account structure along with its newly recovered code,
+// if it exists.
+pub fn from_fat_rlp(
+ acct_db: &mut AccountDBMut,
+ rlp: UntrustedRlp,
+) -> Result<(BasicAccount, Option), Error> {
+ use util::{TrieDBMut, TrieMut};
- Account {
- nonce: r.val_at(0),
- balance: r.val_at(1),
- storage_root: r.val_at(2),
- code_hash: r.val_at(3),
+ // check for special case of empty account.
+ if rlp.is_empty() {
+ return Ok((ACC_EMPTY, None));
+ }
+
+ let nonce = rlp.val_at(0)?;
+ let balance = rlp.val_at(1)?;
+ let code_state: CodeState = {
+ let raw: u8 = rlp.val_at(2)?;
+ CodeState::from(raw)?
+ };
+
+ // load the code if it exists.
+ let (code_hash, new_code) = match code_state {
+ CodeState::Empty => (SHA3_EMPTY, None),
+ CodeState::Inline => {
+ let code: Bytes = rlp.val_at(3)?;
+ let code_hash = acct_db.insert(&code);
+
+ (code_hash, Some(code))
+ }
+ CodeState::Hash => {
+ let code_hash = rlp.val_at(3)?;
+
+ (code_hash, None)
+ }
+ };
+
+ let mut storage_root = H256::zero();
+
+ {
+ let mut storage_trie = TrieDBMut::new(acct_db, &mut storage_root);
+ let pairs = rlp.at(4)?;
+ for pair_rlp in pairs.iter() {
+ let k: Bytes = pair_rlp.val_at(0)?;
+ let v: Bytes = pair_rlp.val_at(1)?;
+
+ storage_trie.insert(&k, &v)?;
}
}
- // encode the account to a standard rlp.
- pub fn to_thin_rlp(&self) -> Bytes {
- let mut stream = RlpStream::new_list(4);
- stream
- .append(&self.nonce)
- .append(&self.balance)
- .append(&self.storage_root)
- .append(&self.code_hash);
+ let acc = BasicAccount {
+ nonce: nonce,
+ balance: balance,
+ storage_root: storage_root,
+ code_hash: code_hash,
+ };
- stream.out()
- }
-
- // walk the account's storage trie, returning an RLP item containing the
- // account properties and the storage.
- pub fn to_fat_rlp(&self, acct_db: &AccountDB, used_code: &mut HashSet) -> Result {
- if self == &ACC_EMPTY {
- return Ok(::rlp::NULL_RLP.to_vec());
- }
-
- let db = TrieDB::new(acct_db, &self.storage_root)?;
-
- let mut pairs = Vec::new();
-
- for item in db.iter()? {
- let (k, v) = item?;
- pairs.push((k, v));
- }
-
- let mut stream = RlpStream::new_list(pairs.len());
-
- for (k, v) in pairs {
- stream.begin_list(2).append(&k).append(&&*v);
- }
-
- let pairs_rlp = stream.out();
-
- let mut account_stream = RlpStream::new_list(5);
- account_stream.append(&self.nonce)
- .append(&self.balance);
-
- // [has_code, code_hash].
- if self.code_hash == SHA3_EMPTY {
- account_stream.append(&CodeState::Empty.raw()).append_empty_data();
- } else if used_code.contains(&self.code_hash) {
- account_stream.append(&CodeState::Hash.raw()).append(&self.code_hash);
- } else {
- match acct_db.get(&self.code_hash) {
- Some(c) => {
- used_code.insert(self.code_hash.clone());
- account_stream.append(&CodeState::Inline.raw()).append(&&*c);
- }
- None => {
- warn!("code lookup failed during snapshot");
- account_stream.append(&false).append_empty_data();
- }
- }
- }
-
- account_stream.append_raw(&pairs_rlp, 1);
-
- Ok(account_stream.out())
- }
-
- // decode a fat rlp, and rebuild the storage trie as we go.
- // returns the account structure along with its newly recovered code,
- // if it exists.
- pub fn from_fat_rlp(
- acct_db: &mut AccountDBMut,
- rlp: UntrustedRlp,
- ) -> Result<(Self, Option), Error> {
- use util::{TrieDBMut, TrieMut};
-
- // check for special case of empty account.
- if rlp.is_empty() {
- return Ok((ACC_EMPTY, None));
- }
-
- let nonce = rlp.val_at(0)?;
- let balance = rlp.val_at(1)?;
- let code_state: CodeState = {
- let raw: u8 = rlp.val_at(2)?;
- CodeState::from(raw)?
- };
-
- // load the code if it exists.
- let (code_hash, new_code) = match code_state {
- CodeState::Empty => (SHA3_EMPTY, None),
- CodeState::Inline => {
- let code: Bytes = rlp.val_at(3)?;
- let code_hash = acct_db.insert(&code);
-
- (code_hash, Some(code))
- }
- CodeState::Hash => {
- let code_hash = rlp.val_at(3)?;
-
- (code_hash, None)
- }
- };
-
- let mut storage_root = H256::zero();
-
- {
- let mut storage_trie = TrieDBMut::new(acct_db, &mut storage_root);
- let pairs = rlp.at(4)?;
- for pair_rlp in pairs.iter() {
- let k: Bytes = pair_rlp.val_at(0)?;
- let v: Bytes = pair_rlp.val_at(1)?;
-
- storage_trie.insert(&k, &v)?;
- }
- }
-
- let acc = Account {
- nonce: nonce,
- balance: balance,
- storage_root: storage_root,
- code_hash: code_hash,
- };
-
- Ok((acc, new_code))
- }
-
- /// Get the account's code hash.
- pub fn code_hash(&self) -> &H256 {
- &self.code_hash
- }
-
- #[cfg(test)]
- pub fn storage_root_mut(&mut self) -> &mut H256 {
- &mut self.storage_root
- }
+ Ok((acc, new_code))
}
#[cfg(test)]
mod tests {
use account_db::{AccountDB, AccountDBMut};
+ use basic_account::BasicAccount;
use tests::helpers::get_temp_state_db;
use snapshot::tests::helpers::fill_storage;
@@ -227,26 +184,26 @@ mod tests {
use std::collections::HashSet;
- use super::{ACC_EMPTY, Account};
+ use super::{ACC_EMPTY, to_fat_rlp, from_fat_rlp};
#[test]
fn encoding_basic() {
let mut db = get_temp_state_db();
let addr = Address::random();
- let account = Account {
+ let account = BasicAccount {
nonce: 50.into(),
balance: 123456789.into(),
storage_root: SHA3_NULL_RLP,
code_hash: SHA3_EMPTY,
};
- let thin_rlp = account.to_thin_rlp();
- assert_eq!(Account::from_thin_rlp(&thin_rlp), account);
+ let thin_rlp = ::rlp::encode(&account);
+ assert_eq!(::rlp::decode::(&thin_rlp), account);
- let fat_rlp = account.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap();
+ let fat_rlp = to_fat_rlp(&account, &AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap();
let fat_rlp = UntrustedRlp::new(&fat_rlp);
- assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp).unwrap().0, account);
+ assert_eq!(from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp).unwrap().0, account);
}
#[test]
@@ -258,7 +215,7 @@ mod tests {
let acct_db = AccountDBMut::new(db.as_hashdb_mut(), &addr);
let mut root = SHA3_NULL_RLP;
fill_storage(acct_db, &mut root, &mut H256::zero());
- Account {
+ BasicAccount {
nonce: 25.into(),
balance: 987654321.into(),
storage_root: root,
@@ -266,12 +223,12 @@ mod tests {
}
};
- let thin_rlp = account.to_thin_rlp();
- assert_eq!(Account::from_thin_rlp(&thin_rlp), account);
+ let thin_rlp = ::rlp::encode(&account);
+ assert_eq!(::rlp::decode::(&thin_rlp), account);
- let fat_rlp = account.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap();
+ let fat_rlp = to_fat_rlp(&account, &AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap();
let fat_rlp = UntrustedRlp::new(&fat_rlp);
- assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp).unwrap().0, account);
+ assert_eq!(from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp).unwrap().0, account);
}
#[test]
@@ -291,14 +248,14 @@ mod tests {
acct_db.emplace(code_hash.clone(), DBValue::from_slice(b"this is definitely code"));
}
- let account1 = Account {
+ let account1 = BasicAccount {
nonce: 50.into(),
balance: 123456789.into(),
storage_root: SHA3_NULL_RLP,
code_hash: code_hash,
};
- let account2 = Account {
+ let account2 = BasicAccount {
nonce: 400.into(),
balance: 98765432123456789usize.into(),
storage_root: SHA3_NULL_RLP,
@@ -307,18 +264,18 @@ mod tests {
let mut used_code = HashSet::new();
- let fat_rlp1 = account1.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr1), &mut used_code).unwrap();
- let fat_rlp2 = account2.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr2), &mut used_code).unwrap();
+ let fat_rlp1 = to_fat_rlp(&account1, &AccountDB::new(db.as_hashdb(), &addr1), &mut used_code).unwrap();
+ let fat_rlp2 = to_fat_rlp(&account2, &AccountDB::new(db.as_hashdb(), &addr2), &mut used_code).unwrap();
assert_eq!(used_code.len(), 1);
let fat_rlp1 = UntrustedRlp::new(&fat_rlp1);
let fat_rlp2 = UntrustedRlp::new(&fat_rlp2);
- let (acc, maybe_code) = Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr2), fat_rlp2).unwrap();
+ let (acc, maybe_code) = from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr2), fat_rlp2).unwrap();
assert!(maybe_code.is_none());
assert_eq!(acc, account2);
- let (acc, maybe_code) = Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr1), fat_rlp1).unwrap();
+ let (acc, maybe_code) = from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr1), fat_rlp1).unwrap();
assert_eq!(maybe_code, Some(b"this is definitely code".to_vec()));
assert_eq!(acc, account1);
}
@@ -328,7 +285,7 @@ mod tests {
let mut db = get_temp_state_db();
let mut used_code = HashSet::new();
- assert_eq!(ACC_EMPTY.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &Address::default()), &mut used_code).unwrap(), ::rlp::NULL_RLP.to_vec());
- assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &Address::default()), UntrustedRlp::new(&::rlp::NULL_RLP)).unwrap(), (ACC_EMPTY, None));
+ assert_eq!(to_fat_rlp(&ACC_EMPTY, &AccountDB::new(db.as_hashdb(), &Address::default()), &mut used_code).unwrap(), ::rlp::NULL_RLP.to_vec());
+ assert_eq!(from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &Address::default()), UntrustedRlp::new(&::rlp::NULL_RLP)).unwrap(), (ACC_EMPTY, None));
}
}
diff --git a/ethcore/src/snapshot/mod.rs b/ethcore/src/snapshot/mod.rs
index 56fd09200..add5029b2 100644
--- a/ethcore/src/snapshot/mod.rs
+++ b/ethcore/src/snapshot/mod.rs
@@ -40,7 +40,6 @@ use util::sha3::SHA3_NULL_RLP;
use rlp::{RlpStream, Stream, UntrustedRlp, View};
use bloom_journal::Bloom;
-use self::account::Account;
use self::block::AbridgedBlock;
use self::io::SnapshotWriter;
@@ -368,12 +367,12 @@ pub fn chunk_state<'a>(db: &HashDB, root: &H256, writer: &Mutex status.new_code.push((code_hash, code, hash)),
@@ -534,7 +533,7 @@ fn rebuild_accounts(
}
}
- acc.to_thin_rlp()
+ ::rlp::encode(&acc).to_vec()
};
*out = (hash, thin_rlp);
diff --git a/ethcore/src/snapshot/tests/helpers.rs b/ethcore/src/snapshot/tests/helpers.rs
index 164f99121..8d7d1bb97 100644
--- a/ethcore/src/snapshot/tests/helpers.rs
+++ b/ethcore/src/snapshot/tests/helpers.rs
@@ -17,9 +17,9 @@
//! Snapshot test helpers. These are used to build blockchains and state tries
//! which can be queried before and after a full snapshot/restore cycle.
+use basic_account::BasicAccount;
use account_db::AccountDBMut;
use rand::Rng;
-use snapshot::account::Account;
use util::DBValue;
use util::hash::{FixedHash, H256};
@@ -64,10 +64,10 @@ impl StateProducer {
// sweep once to alter storage tries.
for &mut (ref mut address_hash, ref mut account_data) in &mut accounts_to_modify {
- let mut account = Account::from_thin_rlp(&*account_data);
+ let mut account: BasicAccount = ::rlp::decode(&*account_data);
let acct_db = AccountDBMut::from_hash(db, *address_hash);
- fill_storage(acct_db, account.storage_root_mut(), &mut self.storage_seed);
- *account_data = DBValue::from_vec(account.to_thin_rlp());
+ fill_storage(acct_db, &mut account.storage_root, &mut self.storage_seed);
+ *account_data = DBValue::from_vec(::rlp::encode(&account).to_vec());
}
// sweep again to alter account trie.
diff --git a/ethcore/src/snapshot/tests/state.rs b/ethcore/src/snapshot/tests/state.rs
index 380e9fb0d..5fcc5a52c 100644
--- a/ethcore/src/snapshot/tests/state.rs
+++ b/ethcore/src/snapshot/tests/state.rs
@@ -16,8 +16,9 @@
//! State snapshotting tests.
+use basic_account::BasicAccount;
+use snapshot::account;
use snapshot::{chunk_state, Error as SnapshotError, Progress, StateRebuilder};
-use snapshot::account::Account;
use snapshot::io::{PackedReader, PackedWriter, SnapshotReader, SnapshotWriter};
use super::helpers::{compare_dbs, StateProducer};
@@ -113,22 +114,21 @@ fn get_code_from_prev_chunk() {
// first one will have code inlined,
// second will just have its hash.
let thin_rlp = acc_stream.out();
- let acc1 = Account::from_thin_rlp(&thin_rlp);
- let acc2 = Account::from_thin_rlp(&thin_rlp);
+ let acc: BasicAccount = ::rlp::decode(&thin_rlp);
- let mut make_chunk = |acc: Account, hash| {
+ let mut make_chunk = |acc, hash| {
let mut db = MemoryDB::new();
AccountDBMut::from_hash(&mut db, hash).insert(&code[..]);
- let fat_rlp = acc.to_fat_rlp(&AccountDB::from_hash(&db, hash), &mut used_code).unwrap();
+ let fat_rlp = account::to_fat_rlp(&acc, &AccountDB::from_hash(&db, hash), &mut used_code).unwrap();
let mut stream = RlpStream::new_list(1);
stream.begin_list(2).append(&hash).append_raw(&fat_rlp, 1);
stream.out()
};
- let chunk1 = make_chunk(acc1, h1);
- let chunk2 = make_chunk(acc2, h2);
+ let chunk1 = make_chunk(acc.clone(), h1);
+ let chunk2 = make_chunk(acc, h2);
let db_path = RandomTempPath::create_dir();
let db_cfg = DatabaseConfig::with_columns(::db::NUM_COLUMNS);
@@ -190,4 +190,4 @@ fn checks_flag() {
}
}
}
-}
\ No newline at end of file
+}
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 3fb04fc36..f9a4b6f6a 100644
--- a/ethcore/src/spec/spec.rs
+++ b/ethcore/src/spec/spec.rs
@@ -58,7 +58,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(),
chain_id: if let Some(n) = p.chain_id { n.into() } else { p.network_id.into() },
diff --git a/ethcore/src/state/account.rs b/ethcore/src/state/account.rs
index 49cebd550..63e8ff9de 100644
--- a/ethcore/src/state/account.rs
+++ b/ethcore/src/state/account.rs
@@ -20,6 +20,7 @@ use util::*;
use pod_account::*;
use rlp::*;
use lru_cache::LruCache;
+use basic_account::BasicAccount;
use std::cell::{RefCell, Cell};
@@ -53,6 +54,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/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
+};
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/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/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/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/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js
index ca4e1f524..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 = (
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;
+ });
});
}
diff --git a/js/src/ui/Form/InputAddress/inputAddress.js b/js/src/ui/Form/InputAddress/inputAddress.js
index 0e49317a1..c10ee126e 100644
--- a/js/src/ui/Form/InputAddress/inputAddress.js
+++ b/js/src/ui/Form/InputAddress/inputAddress.js
@@ -75,6 +75,12 @@ class InputAddress extends Component {
containerClasses.push(styles.small);
}
+ const props = {};
+
+ if (!readOnly && !disabled) {
+ props.focused = focused;
+ }
+
return (
+ }
+ { ...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 { 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/MethodDecoding/methodDecoding.js b/js/src/ui/MethodDecoding/methodDecoding.js
index ad676da0a..a929d2681 100644
--- a/js/src/ui/MethodDecoding/methodDecoding.js
+++ b/js/src/ui/MethodDecoding/methodDecoding.js
@@ -14,12 +14,11 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-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]);
+ }
+
}
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;
});
});
});
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/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');
+ });
+ });
+ });
+});
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;
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..7a895d6dc 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 = {
@@ -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 (
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';
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 aeda7d132..9831ce61e 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,
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 8ac0669d8..a878c2aae 100644
--- a/parity/run.rs
+++ b/parity/run.rs
@@ -58,6 +58,9 @@ const SNAPSHOT_PERIOD: u64 = 10000;
// how many blocks to wait before starting a periodic snapshot.
const SNAPSHOT_HISTORY: u64 = 100;
+// 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.";
+
#[derive(Debug, PartialEq)]
pub struct RunCmd {
pub cache_config: CacheConfig,
@@ -215,7 +218,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());
@@ -228,9 +231,21 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R
miner.set_extra_data(cmd.miner_extras.extra_data);
miner.set_transactions_limit(cmd.miner_extras.transactions_limit);
let engine_signer = cmd.miner_extras.engine_signer;
+
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. {}", build_create_account_hint(&cmd.spec, &cmd.dirs.keys)));
+ }
+
+ // Check if any passwords have been read from the password file(s)
+ if passwords.is_empty() {
+ return Err(format!("No password found for the consensus signer {}. {}", engine_signer, VERIFY_PASSWORD_HINT));
+ }
+
+ // Attempt to sign in the engine signer.
if !passwords.into_iter().any(|p| miner.set_engine_signer(engine_signer, p).is_ok()) {
- return Err(format!("No password found for the consensus signer {}. Make sure valid password is present in files passed using `--password`.", engine_signer));
+ return Err(format!("No valid password for the consensus signer {}. {}", engine_signer, VERIFY_PASSWORD_HINT));
}
}
@@ -463,24 +478,39 @@ 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;
let path = dirs.keys_path(data_dir);
upgrade_key_location(&dirs.legacy_keys_path(cfg.testnet), &path);
let dir = Box::new(DiskDirectory::create(&path).map_err(|e| format!("Could not open keys directory: {}", e))?);
- let account_service = AccountProvider::new(Box::new(
+ let account_provider = AccountProvider::new(Box::new(
EthStore::open_with_iterations(dir, cfg.iterations).map_err(|e| format!("Could not open keys directory: {}", e))?
));
for a in cfg.unlocked_accounts {
- if !passwords.iter().any(|p| account_service.unlock_account_permanently(a, (*p).clone()).is_ok()) {
- return Err(format!("No password found to unlock account {}. Make sure valid password is present in files passed using `--password`.", a));
+ // 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, build_create_account_hint(spec, &dirs.keys)));
+ }
+
+ // Check if any passwords have been read from the password file(s)
+ if passwords.is_empty() {
+ return Err(format!("No password found to unlock account {}. {}", a, VERIFY_PASSWORD_HINT));
+ }
+
+ if !passwords.iter().any(|p| account_provider.unlock_account_permanently(a, (*p).clone()).is_ok()) {
+ return Err(format!("No valid password to unlock account {}. {}", a, VERIFY_PASSWORD_HINT));
}
}
- Ok(account_service)
+ 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(
diff --git a/util/https-fetch/Cargo.toml b/util/https-fetch/Cargo.toml
deleted file mode 100644
index b45904f3a..000000000
--- a/util/https-fetch/Cargo.toml
+++ /dev/null
@@ -1,19 +0,0 @@
-[package]
-description = "HTTPS fetching library"
-homepage = "http://parity.io"
-license = "GPL-3.0"
-name = "https-fetch"
-version = "0.1.0"
-authors = ["Parity Technologies "]
-
-[dependencies]
-log = "0.3"
-ethabi = "0.2.2"
-mio = { git = "https://github.com/ethcore/mio", branch = "v0.5.x" }
-rustls = { git = "https://github.com/ctz/rustls" }
-clippy = { version = "0.0.85", optional = true}
-
-[features]
-default = []
-ca-github-only = []
-dev = ["clippy"]